Docs
Open Studio

API reference

finlab.tools

On this page6

finlab.tools holds two sets of analysis tools. event_study shows whether stocks earn abnormal returns, beyond the benchmark, around a type of event such as earnings announcements. factor_analysis evaluates stock-selection factors: their returns, their correlation with future returns, their contribution inside combinations and their crowding, so you can see where a strategy's performance comes from and which factors to keep.

Which function to use

You want toUse
Build event and future-return datacreate_factor_data()
Compute each event's abnormal return on each day around itevent_study()
Plot the average abnormal return around eventsplot_event_study()
Turn boolean factors into features and next-period excess-return labelsfa.generate_features_and_labels()
Evaluate overall factor performancefa.calc_factor_return()
Evaluate factor correlation with future returnsfa.calc_ic()
Compare how the combinations containing each factor performfa.calc_shapley_values()
Monitor factor crowdingfa.calc_centrality()
Detect trends in centrality, IC or contributionsfa.calc_regression_stats()

Study price reactions around events

An event study here uses a CAPM model: for each event, beta is fitted on the stock's daily returns against a benchmark over a window before the event, and the abnormal return (actual return minus beta times the benchmark return) is reported for each day around the event. Use it to analyze the short-term and long-term impact of events such as earnings announcements, investor conferences and dividend distributions.

How do S&P 500 stocks move around their earnings announcements?

from finlab import data
from finlab.tools.event_study import create_factor_data, event_study

data.set_market('us')
with data.us_universe(index='SP500'):
    adj_close = data.get('price:adj_close')
    eps_actual = data.get('us_earnings_surprises:eps_actual')
    eps_estimated = data.get('us_earnings_surprises:eps_estimated')
benchmark = data.get('world_index:adj_close')[
    ['^GSPC']
]  # S&P 500, one-column DataFrame

# Events: earnings announcements; leave room for the 45-day CAPM estimation window
event = eps_actual.notna().loc['2016-06':]
surprise = (eps_actual - eps_estimated) / eps_estimated.abs()

factor_data = create_factor_data({'surprise': surprise}, adj_close, event=event)
abnormal_returns = event_study(factor_data, benchmark, adj_close)

create_factor_data()

Builds the factor data used by event_study(): factor values, factor quintiles and future returns for each (date, asset).

from finlab.tools.event_study import create_factor_data

factor_data = create_factor_data({'surprise': surprise}, adj_close, event=event)
factor_data.head()
ParameterTypeDefaultDescription
factorDataFrame or dict[str, DataFrame]requiredFactor values, index = dates, columns = asset ids. A dict names several factors.
adj_closeDataFramerequiredAdjusted close prices, index = dates, columns = asset ids.
dayslist[int]NoneFuture-return horizons in trading days; None uses [5, 10, 20, 60].
eventboolean DataFrameNoneEvent flags (index = event dates, columns = asset ids). When given, only (date, asset) pairs with an event are kept.

Returns a DataFrame indexed by (date, asset) with <name>_factor, <name>_factor_quantile (the cross-sectional quintile on each date, 1 = lowest to 5 = highest, like df.cs.bucket(5)) and one <n>D future-return column per horizon. Event dates that fall on non-trading days move to the next trading day in adj_close.

This function is not identical to finlab.ml.alphalens.create_factor_data.

event_study()

Fits beta before each event and returns the abnormal return of each event on each day around it.

from finlab.tools.event_study import event_study

abnormal_returns = event_study(factor_data, benchmark, adj_close, plot=False)
abnormal_returns.mean()  # Average abnormal return per day
ParameterTypeDefaultDescription
factor_dataDataFramerequiredOutput of create_factor_data(); its (date, asset) index lists the events.
benchmark_adj_closeDataFramerequiredBenchmark prices for the CAPM fit; the first column is used.
stock_adj_closeDataFramerequiredStock prices, index = dates, columns = asset ids.
sample_period(int, int)(-45, -20)Days relative to the event used to fit beta.
estimation_period(int, int)(-5, 20)Days relative to the event for which abnormal returns are computed.
plotboolTruePlot the mean daily and cumulative abnormal return with plot_event_study().

Returns a DataFrame with one row per event and one column per day of estimation_period (for the defaults, -5 to 19), holding the abnormal returns.

plot_event_study()

Plots the mean abnormal return per day as bars and the mean cumulative abnormal return as a line with a 0.1 standard deviation band.

from finlab.tools.event_study import plot_event_study

plot_event_study(abnormal_returns)
ParameterTypeDefaultDescription
returnsDataFramerequiredThe output of event_study().

Returns a matplotlib.axes.Axes.

Analyze factors

The main factor_analysis functions take features (a DataFrame indexed by (date, stock) with one column per factor) and labels (a Series with the same index, typically next-period excess returns), as built by generate_features_and_labels() or by finlab.ml.feature.combine() and finlab.ml.label.excess_over_mean().

module

finlab.tools.factor_analysis

Factor analysis toolkit -- public facade.

All public symbols are re-exported from focused submodules so that existing from finlab.tools.factor_analysis import ... imports keep working unchanged.

Submodules

  • finlab.tools.factor_metrics -- IC, correlation, NDCG scoring
  • finlab.tools.factor_returns -- boolean factor returns & Shapley values
  • finlab.tools.factor_centrality -- PCA-based rolling centrality
  • finlab.tools.factor_regression -- OLS trend analysis

fa.generate_features_and_labels()

Turns a dict of factors into a feature table and excess-return labels.

from finlab import data
from finlab.tools import factor_analysis as fa

data.set_market('us')
with data.us_universe(index='SP500'):
    marketcap = data.get('etl:us_market_value')
    revenue = data.get('us_income_statement:revenue')
    close = data.get('price:adj_close')

# Boolean factors -> features (date x stock) and next-month excess-return labels
factors = {
    'small_cap': marketcap.rank(pct=True, axis=1) < 0.3,
    'revenue_growth': (revenue / revenue.shift(4)).rank(pct=True, axis=1) > 0.7,
    'momentum': (close / close.shift(20)).rank(pct=True, axis=1) > 0.7,
}
features, labels = fa.generate_features_and_labels(factors, resample='ME')
ParameterTypeDefaultDescription
dfsdictrequiredFactor name to DataFrame (or a callable that returns one).
resamplestrrequiredSampling frequency passed to feature.combine() and label.excess_over_mean(), e.g. 'ME' for month end.

Returns (features, labels). The examples below continue from these features, labels and factors.

fa.calc_factor_return()

Computes the equal-weight return of the stocks each boolean factor selects, per date, to evaluate overall factor performance.

factor_return = fa.calc_factor_return(features, labels)
factor_return.cumsum().plot(title='Factor cumulative return')
print(factor_return.mean())  # Average excess return of the selected stocks, per month
ParameterTypeDefaultDescription
featuresDataFramerequiredBoolean features indexed by (date, stock). A column that is not boolean raises ValueError.
labelsSeriesrequiredExcess-return labels with the same index.

Returns a table of per-date returns, one column per factor. How to read it:

  • Positive: factor has stock selection ability (selected stocks outperform)
  • Negative: factor works in reverse (should inverse the signal)
  • Near 0: factor has no stock selection ability

fa.calc_ic()

Computes the correlation (IC) between factor values and labels per date, to evaluate factor correlation with future returns. IC needs continuous factor values, not booleans:

from finlab.ml import feature

features_raw = feature.combine(
    {
        'small_cap': -marketcap,
        'revenue_growth': revenue / revenue.shift(4),
        'momentum': close / close.shift(20),
    },
    resample='ME',
)
ic = fa.calc_ic(features_raw, labels, rank=True)
print(ic.mean() / ic.std())  # IC_IR
ParameterTypeDefaultDescription
featuresDataFramerequiredFactor values indexed by (date, stock).
labelsSeriesrequiredLabels with the same index.
rankboolFalseTrue computes Spearman rank correlation; otherwise Pearson.

Returns a table of IC values per date, one column per factor. IC evaluation standards:

IC ValueRatingDescription
> 0.05ExcellentFactor has strong stock selection ability
0.03 ~ 0.05GoodFactor has stock selection ability
0.01 ~ 0.03AverageFactor has weak stock selection ability
< 0.01IneffectiveFactor has no stock selection ability

fa.calc_shapley_values()

Computes each boolean factor's contribution per date: each factor combination's return is split evenly among its factors.

shapley = fa.calc_shapley_values(features, labels)
print(shapley.mean())
ParameterTypeDefaultDescription
featuresDataFramerequiredBoolean features indexed by (date, stock).
labelsSeriesrequiredExcess-return labels with the same index.

Returns a table of per-date contributions, one column per factor. For how it is computed and how it differs from the game-theoretic Shapley value, see Analyzing strategy factors.

fa.calc_centrality()

Computes the rolling PCA centrality of factor returns, to monitor factor crowding.

# Centrality (crowding) over a 12-month window
centrality = fa.calc_centrality(factor_return, 12)
ParameterTypeDefaultDescription
return_dfDataFramerequiredDate-indexed returns, such as the output of calc_factor_return().
window_periodsintrequiredRolling window length in rows.
n_componentsint1Number of PCA components.

fa.calc_regression_stats()

Fits a linear trend to each column over time and classifies it as up, down or flat, to detect trends in centrality, IC or contributions.

print(fa.calc_regression_stats(centrality))
ParameterTypeDefaultDescription
dfDataFramerequiredTime series with a DatetimeIndex.
p_value_thresholdfloat0.05Significance level for the trend.
r_squared_thresholdfloat0.1Minimum R-squared for a trend that is not flat.

Returns one row per column with slope, intercept, r_squared, p_value, tail_estimate and trend.

Complete example: from factor analysis to a backtest

from finlab import data
from finlab.backtest import sim
from finlab.tools import factor_analysis as fa

# Step 1: Build factors
data.set_market('us')
with data.us_universe(index='SP500'):
    marketcap = data.get('etl:us_market_value')
    revenue = data.get('us_income_statement:revenue')
    close = data.get('price:adj_close')

factors = {
    'small_cap': marketcap.rank(pct=True, axis=1) < 0.3,
    'revenue_growth': (revenue / revenue.shift(4)).rank(pct=True, axis=1) > 0.7,
    'momentum': (close / close.shift(20)).rank(pct=True, axis=1) > 0.7,
}

# Step 2: Features and labels (next-month excess return)
features, labels = fa.generate_features_and_labels(factors, resample='ME')

# Step 3: Analyze each factor
print("=== Average monthly factor return ===")
print(fa.calc_factor_return(features, labels).mean())

# Step 4: Shapley values (contribution)
print("=== Average Shapley value ===")
print(fa.calc_shapley_values(features, labels).mean())

# Step 5: Backtest the combined strategy
position = factors['small_cap'] & factors['revenue_growth'] & factors['momentum']
report = sim(position, resample='M')
report.display()

stats = report.get_stats()
print(f"Annual return: {stats['cagr']:.2%}")
print(f"Sharpe ratio: {stats['daily_sharpe']:.2f}")
print(f"Max drawdown: {stats['max_drawdown']:.2%}")

Common questions

How do I tell whether a factor is effective?

Use three metrics together:

# 1. Factor return (should be clearly > 0)
print(fa.calc_factor_return(features, labels).mean())

# 2. IC (should be > 0.02)
ic = fa.calc_ic(features_raw, labels, rank=True)
print(ic.mean())

# 3. IC_IR (should be > 0.5)
print(ic.mean() / ic.std())

# Conclusion: Factor is effective only when all three metrics pass
How do I apply Shapley values to strategy optimization?
# Average contribution of each factor
contribution = fa.calc_shapley_values(features, labels).mean()

# Keep factors with a positive contribution; keep the best one if none is positive
kept = contribution[contribution > 0].index
if kept.empty:
    kept = contribution.nlargest(1).index

print(f"Original factor count: {len(contribution)}")
print(f"Optimized factor count: {len(kept)}")

# Combine the kept factors
optimized_position = factors[kept[0]]
for name in kept[1:]:
    optimized_position = optimized_position & factors[name]
What is the difference between IC and factor return?
  • IC (Information Coefficient):

    • Measures the correlation between factor values and future returns
    • Range [-1, 1], closer to 1 is better
    • Suitable for evaluating continuous factors (e.g., market cap, ROE)
  • Factor return:

    • Measures the average return of stocks selected by the factor
    • Suitable for evaluating binary factors (True/False)
    • Directly reflects stock selection effectiveness
from finlab.ml import feature

eps = data.get('us_income_statement:eps_diluted')
raw_close = data.get('price:close')  # unadjusted price for valuation
earnings_yield = eps.rolling(4).sum() / raw_close  # trailing-12-month E/P

# Use IC for continuous factors
continuous = feature.combine({'earnings_yield': earnings_yield}, resample='ME')
ic = fa.calc_ic(continuous, labels, rank=True)

# Use factor return for binary factors
binary = feature.combine(
    {'earnings_yield': earnings_yield.rank(pct=True, axis=1) > 0.7}, resample='ME'
)
factor_return = fa.calc_factor_return(binary, labels)

Next steps

↑↓ Move Enter Open Esc Close