finlab.tools
Utility module providing event study and factor analysis functionality.
Use Cases
- Analyze the impact of specific events on stock prices (event study)
- Evaluate factor stock selection ability (factor analysis)
- Optimize factor combinations in strategies
- Understand the sources of strategy performance
Quick Examples
Event Study
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)
Factor Analysis
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
features, labels = fa.generate_features_and_labels(
{
'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,
},
resample='ME',
)
factor_return = fa.calc_factor_return(features, labels)
factor_return.cumsum().plot(title='Factor cumulative return')
Detailed Guide
See Factor Analysis Tutorial for: - Complete factor analysis workflow - Factor return calculation - Factor IC analysis - Shapley values contribution analysis
API Reference
finlab.tools.event_study
Event study with 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.
create_factor_data()
create_factor_data(
factor: pd.DataFrame | dict[str, pd.DataFrame],
adj_close: pd.DataFrame,
days: Sequence[int] | None = None,
event: pd.DataFrame | None = None,
) -> pd.DataFrame
Builds the factor data used by event_study(): factor values, factor quintiles and future returns for each (date, asset).
| Parameter | Type | Description |
|---|---|---|
factor |
DataFrame or dict[str, DataFrame] |
Factor values, index = dates, columns = asset ids. A dict names several factors |
adj_close |
DataFrame |
Adjusted close prices, index = dates, columns = asset ids |
days |
list of int, optional |
Future-return horizons in trading days; defaults to [5, 10, 20, 60] |
event |
boolean DataFrame, optional |
Event 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()
event_study(
factor_data: pd.DataFrame,
benchmark_adj_close: pd.DataFrame,
stock_adj_close: pd.DataFrame,
sample_period: tuple[int, int] = (-45, -20),
estimation_period: tuple[int, int] = (-5, 20),
plot: bool = True,
) -> pd.DataFrame
| Parameter | Type | Description |
|---|---|---|
factor_data |
DataFrame |
Output of create_factor_data(); its (date, asset) index lists the events |
benchmark_adj_close |
DataFrame |
Benchmark prices for the CAPM fit; the first column is used |
stock_adj_close |
DataFrame |
Stock prices, index = dates, columns = asset ids |
sample_period |
(int, int) |
Days relative to the event used to fit beta |
estimation_period |
(int, int) |
Days relative to the event for which abnormal returns are computed |
plot |
bool |
Plot 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. returns is the output of event_study().
Usage Notes
Event study is used to analyze the short-term and long-term impact of specific events (such as earnings announcements, investor conferences, dividend distributions) on stock prices.
finlab.tools.factor_analysis
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
- :mod:
finlab.tools.factor_metrics-- IC, correlation, NDCG scoring - :mod:
finlab.tools.factor_returns-- boolean factor returns & Shapley values - :mod:
finlab.tools.factor_centrality-- PCA-based rolling centrality - :mod:
finlab.tools.factor_regression-- OLS trend analysis
calc_centrality
Compute rolling PCA centrality over return_df.
| PARAMETER | DESCRIPTION |
|---|---|
return_df
|
Time-series DataFrame (dates x assets/factors).
TYPE:
|
window_periods
|
Rolling window length in rows.
TYPE:
|
n_components
|
Number of PCA components.
TYPE:
|
| RETURNS | DESCRIPTION |
|---|---|
DataFrame
|
DataFrame of rolling centrality scores. |
calc_factor_return
Compute equal-weight portfolio returns per boolean factor.
Each column in features must be boolean. For every date the mean
label value across selected (True) stocks is returned.
| PARAMETER | DESCRIPTION |
|---|---|
features
|
Boolean feature DataFrame (MultiIndex: date x stock).
TYPE:
|
labels
|
Excess-return labels with the same MultiIndex.
TYPE:
|
| RETURNS | DESCRIPTION |
|---|---|
DataFrame
|
DataFrame of per-period returns, one column per factor, |
DataFrame
|
starting from the first fully-populated date. |
| RAISES | DESCRIPTION |
|---|---|
ValueError
|
If any feature column is not boolean. |
calc_ic
Compute per-date IC between features and labels.
| PARAMETER | DESCRIPTION |
|---|---|
features
|
MultiIndex DataFrame (date, stock) with factor columns.
TYPE:
|
labels
|
MultiIndex Series with the same index.
TYPE:
|
rank
|
If
TYPE:
|
| RETURNS | DESCRIPTION |
|---|---|
DataFrame
|
DataFrame of IC values per date and factor. The Pearson result starts |
DataFrame
|
at the first date where every factor that has any IC has one; factors |
DataFrame
|
with no IC at all stay as all-NaN columns. |
calc_metric
Compute a cross-sectional metric between factor and forward returns.
| PARAMETER | DESCRIPTION |
|---|---|
factor
|
Single factor DataFrame or dict of named factor DataFrames.
TYPE:
|
adj_close
|
Adjusted close prices.
TYPE:
|
days
|
Forward-return horizons (default
TYPE:
|
func
|
Scoring callable applied per date group (default :func:
TYPE:
|
| RETURNS | DESCRIPTION |
|---|---|
DataFrame
|
DataFrame with one column per |
calc_regression_stats
Run per-column OLS regressions and classify each trend.
| PARAMETER | DESCRIPTION |
|---|---|
df
|
Time-series DataFrame with a
TYPE:
|
p_value_threshold
|
Significance level for trend classification.
TYPE:
|
r_squared_threshold
|
Minimum R-squared for a non-flat trend.
TYPE:
|
| RETURNS | DESCRIPTION |
|---|---|
DataFrame
|
DataFrame with columns |
DataFrame
|
|
| RAISES | DESCRIPTION |
|---|---|
ValueError
|
If the DataFrame index is not a |
calc_shapley_values
Score each boolean factor by the returns of the combinations containing it.
Enumerates all 2^n - 1 non-empty subsets of factors. For each subset and date, takes the mean label of the stocks selected by every factor in the subset (0 when none) and splits it evenly among the subset's factors. A factor's value is the sum of its shares over all subsets containing 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 values do not sum to the return of the all-factor subset.
| PARAMETER | DESCRIPTION |
|---|---|
features
|
Boolean feature DataFrame (MultiIndex: date x stock).
TYPE:
|
labels
|
Excess-return labels with the same MultiIndex.
TYPE:
|
| RETURNS | DESCRIPTION |
|---|---|
DataFrame
|
DataFrame of per-date scores, one column per factor. |
| RAISES | DESCRIPTION |
|---|---|
ValueError
|
If features is empty, labels lacks a MultiIndex, or any feature column is not boolean. |
generate_features_and_labels
Build a feature matrix and an excess-return label vector.
Wraps :func:finlab.ml.feature.combine and
:func:finlab.ml.label.excess_over_mean.
| PARAMETER | DESCRIPTION |
|---|---|
dfs
|
Dict mapping factor names to DataFrames (or callables).
TYPE:
|
resample
|
Resampling frequency / index passed to the ML helpers.
TYPE:
|
| RETURNS | DESCRIPTION |
|---|---|
tuple[DataFrame, Series]
|
|
ic
Per-date IC between factor and forward returns.
| PARAMETER | DESCRIPTION |
|---|---|
factor
|
Single factor DataFrame or dict of named factor DataFrames.
TYPE:
|
adj_close
|
Adjusted close prices.
TYPE:
|
days
|
Forward-return horizons (default
TYPE:
|
method
|
TYPE:
|
| RETURNS | DESCRIPTION |
|---|---|
DataFrame
|
DataFrame with one column per |
is_boolean_series
Check whether series contains only boolean-like values (including NaN).
Handles the case where older pandas converts bool + NaN to float.
Factor analysis module for evaluating factor stock selection ability and contribution. The main 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().
| Function | Description | Purpose |
|---|---|---|
calc_factor_return(features, labels) |
Equal-weight return of the stocks each boolean factor selects, per date | Evaluate overall factor performance |
calc_ic(features, labels, rank=False) |
Correlation between factor values and labels per date (rank=True: Spearman) |
Evaluate factor correlation with future returns |
calc_shapley_values(features, labels) |
Contribution of each boolean factor per date: each factor combination's return split evenly among its factors | Compare how the combinations containing each factor perform |
calc_centrality(return_df, window_periods) |
Rolling PCA centrality of factor returns | Monitor factor crowding |
calc_regression_stats(df) |
Linear trend of each column over time (up / down / flat) |
Detect trends in centrality, IC or contributions |
Usage
Continuing from the quick example above:
from finlab.ml import feature
# Factor return: equal-weight excess return of the selected stocks, per month
factor_return = fa.calc_factor_return(features, labels)
print(factor_return.mean())
# IC needs continuous factor values, not booleans
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
# Contribution per month: each combination's return split among its factors
shapley = fa.calc_shapley_values(features, labels)
# Centrality (crowding) over a 12-month window, and its trend
centrality = fa.calc_centrality(factor_return, 12)
print(fa.calc_regression_stats(centrality))
Interpreting factor returns: - 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
IC Evaluation Standards:
| IC Value | Rating | Description |
|---|---|---|
| > 0.05 | Excellent | Factor has strong stock selection ability |
| 0.03 ~ 0.05 | Good | Factor has stock selection ability |
| 0.01 ~ 0.03 | Average | Factor has weak stock selection ability |
| < 0.01 | Ineffective | Factor has no stock selection ability |
IC Analysis Notes
- IC > 0: Factor positively correlates with future returns (go long on high-scoring stocks)
- IC < 0: Factor negatively correlates with future returns (go long on low-scoring stocks)
- High IC variance: Factor is unstable, higher risk
- IC_IR > 0.5: Factor performs well on a risk-adjusted basis
Best Practices
- Use long backtest periods to verify factor stability
- Compare factor performance across different market environments (bull/bear)
- Remove factors with negative or near-zero Shapley values, then re-evaluate the strategy
Complete Example: Multi-Factor Strategy Analysis
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%}")
FAQ
Q: How do I determine if a factor is effective?
Use three metrics for comprehensive evaluation:
# 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
Q: 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]
Q: 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)
Resources
- Factor Analysis Tutorial - End-to-end examples
- Complete Strategy Development Workflow - Includes factor analysis steps
- Machine Learning Strategies - Use ML to automatically discover factors