Skip to content

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()

plot_event_study(returns: pd.DataFrame) -> matplotlib.axes.Axes

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

calc_centrality(return_df, window_periods, n_components=1)

Compute rolling PCA centrality over return_df.

PARAMETER DESCRIPTION
return_df

Time-series DataFrame (dates x assets/factors).

TYPE: DataFrame

window_periods

Rolling window length in rows.

TYPE: int

n_components

Number of PCA components.

TYPE: int DEFAULT: 1

RETURNS DESCRIPTION
DataFrame

DataFrame of rolling centrality scores.

calc_factor_return

calc_factor_return(features, labels)

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: DataFrame

labels

Excess-return labels with the same MultiIndex.

TYPE: Series

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

calc_ic(features, labels, rank=False)

Compute per-date IC between features and labels.

PARAMETER DESCRIPTION
features

MultiIndex DataFrame (date, stock) with factor columns.

TYPE: DataFrame

labels

MultiIndex Series with the same index.

TYPE: Series

rank

If True, compute Spearman rank IC (features and labels are both ranked per date); otherwise Pearson IC.

TYPE: bool DEFAULT: False

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

calc_metric(factor, adj_close, days=None, func=corr)

Compute a cross-sectional metric between factor and forward returns.

PARAMETER DESCRIPTION
factor

Single factor DataFrame or dict of named factor DataFrames.

TYPE: DataFrame | dict[str, DataFrame]

adj_close

Adjusted close prices.

TYPE: DataFrame

days

Forward-return horizons (default [10, 20, 60, 120]).

TYPE: list[int] | None DEFAULT: None

func

Scoring callable applied per date group (default :func:corr).

TYPE: Callable[[DataFrame], float] DEFAULT: corr

RETURNS DESCRIPTION
DataFrame

DataFrame with one column per (factor_name, horizon) pair.

calc_regression_stats

calc_regression_stats(
    df, p_value_threshold=0.05, r_squared_threshold=0.1
)

Run per-column OLS regressions and classify each trend.

PARAMETER DESCRIPTION
df

Time-series DataFrame with a DatetimeIndex.

TYPE: DataFrame

p_value_threshold

Significance level for trend classification.

TYPE: float DEFAULT: 0.05

r_squared_threshold

Minimum R-squared for a non-flat trend.

TYPE: float DEFAULT: 0.1

RETURNS DESCRIPTION
DataFrame

DataFrame with columns slope, intercept, r_squared,

DataFrame

p_value, tail_estimate, and trend ("up"/"down"/"flat").

RAISES DESCRIPTION
ValueError

If the DataFrame index is not a DatetimeIndex.

calc_shapley_values

calc_shapley_values(features, labels)

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: DataFrame

labels

Excess-return labels with the same MultiIndex.

TYPE: Series

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.

corr

corr(df)

Pearson correlation between the first two columns of df.

generate_features_and_labels

generate_features_and_labels(dfs, resample)

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: dict[str, DataFrame | Callable[[], DataFrame]]

resample

Resampling frequency / index passed to the ML helpers.

TYPE: str

RETURNS DESCRIPTION
tuple[DataFrame, Series]

(features, labels) tuple.

ic

ic(factor, adj_close, days=None, method='pearson')

Per-date IC between factor and forward returns.

PARAMETER DESCRIPTION
factor

Single factor DataFrame or dict of named factor DataFrames.

TYPE: DataFrame | dict[str, DataFrame]

adj_close

Adjusted close prices.

TYPE: DataFrame

days

Forward-return horizons (default [10, 20, 60, 120]).

TYPE: list[int] | None DEFAULT: None

method

"pearson" (default, unchanged) or "spearman" for rank IC.

TYPE: CorrelationMethod DEFAULT: 'pearson'

RETURNS DESCRIPTION
DataFrame

DataFrame with one column per (factor_name, horizon) pair.

is_boolean_series

is_boolean_series(series)

Check whether series contains only boolean-like values (including NaN).

Handles the case where older pandas converts bool + NaN to float.

ndcg_k

ndcg_k(k)

Return an NDCG scorer truncated at rank k.

precision_at_rank

precision_at_rank(k)

Return a precision scorer that evaluates the top 1 - k quantile.

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