Using Alphalens to Analyze Cross-Sectional Factor Signals
Summary
This notebook walks through an Alphalens workflow for assessing alpha factors, which assign a value to each asset at each date and are judged by how those relative values relate to subsequent returns. It demonstrates loading daily stock prices, organizing factor and price data, assigning sector groupings, and formatting the inputs for analysis. The example uses next-day open prices as entry prices and later opens to calculate forward returns, and it stresses that prices used for returns must not leak into the factor calculation.
The tear sheet methods include returns by factor quantile, cumulative quantile returns, top-to-bottom spreads, information coefficients, factor rank autocorrelation, turnover, event-window returns, and groupwise views. However, the example explicitly constructs its signal from future price changes, making it deliberately look-ahead biased; its apparent predictive quality cannot support investment conclusions. The notebook also notes that input formatting can drop observations and that Alphalens does not assess transaction costs, capacity, execution, or portfolio construction. The tools measure signal behavior, not a complete strategy’s tradability or profitability.
Key ideas
- An alpha factor assigns a value to each asset and date, and its predictive ability is evaluated using later returns.
- Factor and price data must be aligned so that return prices follow the signal observation.
- Alphalens provides quantile return, spread, information coefficient, turnover, persistence, and event analyses.
- Sector mappings allow factor behavior to be compared across groups.
- The notebook’s example signal uses future prices and is intentionally affected by look-ahead bias.
- Factor tear sheets do not measure implementation costs, capacity, or portfolio construction.
Tags
Full text
# Alphalens Example Tear Sheet
# Alphalens Example Tear Sheet
Alphalens is designed to aid in the analysis of "alpha factors," data transformations that are used to predict future price movements of financial instruments. Alpha factors take the form of a single value for each asset on each day. The dimension of these values is not necessarily important. We evaluate an alpha factor by considering daily factor values relative to one another.
It is important to note the difference between an alpha factor and a trading algorithm. A trading algorithm uses an alpha factor, or combination of alpha factors to generate trades. Trading algorithms cover execution and risk constraints: the business of turning predictions into profits. Alpha factors, on the other hand, are focused soley on making predictions. This difference in scope lends itself to a difference in the methodologies used to evaluate alpha factors and trading algorithms. Alphalens does not contain analyses of things like transaction costs, capacity, or portfolio construction. Those interested in more implementation specific analyses are encouaged to check out pyfolio (https://github.com/quantopian/pyfolio), a library specifically geared towards the evaluation of trading algorithms.
```python
%pylab inline --no-import-all
import alphalens
import pandas as pd
import numpy as np
```
```python
import warnings
warnings.filterwarnings('ignore')
```
```python
ticker_sector = {
"ACN" : 0, "ATVI" : 0, "ADBE" : 0, "AMD" : 0, "AKAM" : 0, "ADS" : 0, "GOOGL" : 0, "GOOG" : 0,
"APH" : 0, "ADI" : 0, "ANSS" : 0, "AAPL" : 0, "AMAT" : 0, "ADSK" : 0, "ADP" : 0, "AVGO" : 0,
"AMG" : 1, "AFL" : 1, "ALL" : 1, "AXP" : 1, "AIG" : 1, "AMP" : 1, "AON" : 1, "AJG" : 1, "AIZ" : 1, "BAC" : 1,
"BK" : 1, "BBT" : 1, "BRK.B" : 1, "BLK" : 1, "HRB" : 1, "BHF" : 1, "COF" : 1, "CBOE" : 1, "SCHW" : 1, "CB" : 1,
"ABT" : 2, "ABBV" : 2, "AET" : 2, "A" : 2, "ALXN" : 2, "ALGN" : 2, "AGN" : 2, "ABC" : 2, "AMGN" : 2, "ANTM" : 2,
"BCR" : 2, "BAX" : 2, "BDX" : 2, "BIIB" : 2, "BSX" : 2, "BMY" : 2, "CAH" : 2, "CELG" : 2, "CNC" : 2, "CERN" : 2,
"MMM" : 3, "AYI" : 3, "ALK" : 3, "ALLE" : 3, "AAL" : 3, "AME" : 3, "AOS" : 3, "ARNC" : 3, "BA" : 3, "CHRW" : 3,
"CAT" : 3, "CTAS" : 3, "CSX" : 3, "CMI" : 3, "DE" : 3, "DAL" : 3, "DOV" : 3, "ETN" : 3, "EMR" : 3, "EFX" : 3,
"AES" : 4, "LNT" : 4, "AEE" : 4, "AEP" : 4, "AWK" : 4, "CNP" : 4, "CMS" : 4, "ED" : 4, "D" : 4, "DTE" : 4,
"DUK" : 4, "EIX" : 4, "ETR" : 4, "ES" : 4, "EXC" : 4, "FE" : 4, "NEE" : 4, "NI" : 4, "NRG" : 4, "PCG" : 4,
"ARE" : 5, "AMT" : 5, "AIV" : 5, "AVB" : 5, "BXP" : 5, "CBG" : 5, "CCI" : 5, "DLR" : 5, "DRE" : 5,
"EQIX" : 5, "EQR" : 5, "ESS" : 5, "EXR" : 5, "FRT" : 5, "GGP" : 5, "HCP" : 5, "HST" : 5, "IRM" : 5, "KIM" : 5,
"APD" : 6, "ALB" : 6, "AVY" : 6, "BLL" : 6, "CF" : 6, "DWDP" : 6, "EMN" : 6, "ECL" : 6, "FMC" : 6, "FCX" : 6,
"IP" : 6, "IFF" : 6, "LYB" : 6, "MLM" : 6, "MON" : 6, "MOS" : 6, "NEM" : 6, "NUE" : 6, "PKG" : 6, "PPG" : 6,
"T" : 7, "CTL" : 7, "VZ" : 7,
"MO" : 8, "ADM" : 8, "BF.B" : 8, "CPB" : 8, "CHD" : 8, "CLX" : 8, "KO" : 8, "CL" : 8, "CAG" : 8,
"STZ" : 8, "COST" : 8, "COTY" : 8, "CVS" : 8, "DPS" : 8, "EL" : 8, "GIS" : 8, "HSY" : 8, "HRL" : 8,
"AAP" : 9, "AMZN" : 9, "APTV" : 9, "AZO" : 9, "BBY" : 9, "BWA" : 9, "KMX" : 9, "CCL" : 9,
"APC" : 10, "ANDV" : 10, "APA" : 10, "BHGE" : 10, "COG" : 10, "CHK" : 10, "CVX" : 10, "XEC" : 10, "CXO" : 10,
"COP" : 10, "DVN" : 10, "EOG" : 10, "EQT" : 10, "XOM" : 10, "HAL" : 10, "HP" : 10, "HES" : 10, "KMI" : 10
}
```
```python
import pandas_datareader.data as web
pan = web.DataReader(list(ticker_sector.keys()), "yahoo", '2014-12-01', '2017-07-01')
```
```python
pan = pan.transpose(2,1,0)
```
For demonstration purposes we will create a predictive factor. To cheat we will look at future prices to make sure we'll rank high stoks that will perform well and vice versa.
```python
lookahead_bias_days = 5
predictive_factor = pan.loc[:,:,'Open']
predictive_factor = predictive_factor.pct_change(lookahead_bias_days)
# introduce look-ahead bias and make the factor predictive
predictive_factor = predictive_factor.shift(-lookahead_bias_days)
predictive_factor = predictive_factor.stack()
predictive_factor.index = predictive_factor.index.set_names(['date', 'asset'])
```
```python
predictive_factor.head()
```
The pricing data passed to alphalens should contain the entry price for the assets so it must reflect the next available price after a factor value was observed at a given timestamp. Those prices must not be used in the calculation of the factor values for that time. Always double check to ensure you are not introducing lookahead bias to your study.
The pricing data must also contain the exit price for the assets, for period 1 the price at the next timestamp will be used, for period 2 the price after 2 timestats will be used and so on.
There are no restrinctions/assumptions on the time frequencies a factor should be computed at and neither on the specific time a factor should be traded (trading at the open vs trading at the close vs intraday trading), it is only required that factor and price DataFrames are properly aligned given the rules above.
In our example, before the trading starts every day, we observe yesterday factor values. The price we pass to alphalens is the next available price after that factor observation: the daily open price that will be used as assets entry price. Also, we are not adding additional prices so the assets exit price will be the following days open prices (how many days depends on 'periods' argument). The retuns computed by Alphalens will therefore based on assets open prices.
```python
pricing = pan.loc[:,:,'Open'].iloc[1:]
pricing.head()
```
Often, we'd want to know how our factor looks across various groupings (sectors, industires, countries, etc.), in this example let's use sectors. To generate sector level breakdowns, you'll need to pass alphalens a sector mapping for each traded name.
This mapping can come in the form of a MultiIndexed Series (with the same date/symbol index as your factor value) if you want to provide a sector mapping for each symbol on each day.
If you'd like to use constant sector mappings, you may pass symbol to sector mappings as a dict.
If your sector mappings come in the form of codes (as they do in this tutorial), you may also pass alphalens a dict of sector names to use in place of sector codes.
```python
sector_names = {
0 : "information_technology",
1 : "financials",
2 : "health_care",
3 : "industrials",
4 : "utilities",
5 : "real_estate",
6 : "materials",
7 : "telecommunication_services",
8 : "consumer_staples",
9 : "consumer_discretionary",
10 : "energy"
}
```
## Formatting input data
Alphalens contains a handy data formatting function to transform your factor and pricing data into the exact inputs expected by the tear sheet functions.
```python
factor_data = alphalens.utils.get_clean_factor_and_forward_returns(predictive_factor,
pricing,
quantiles=5,
bins=None,
groupby=ticker_sector,
groupby_labels=sector_names)
```
```python
```
The function inform the user how much data was dropped after formatting the input data. Factor data can be partially dropped due to being flawed itself (e.g. NaNs), not having provided enough price data to compute forward returns for all factor values, or because it is not possible to perform binning. It is possible to control the maximum allowed data loss using 'max_loss' argument.
```python
factor_data.head()
```
You'll notice that we've placed all of the information we need for our calculations into one dataframe. Variables are the columns, and observations are each row.
The integer columns represents the forward returns or the daily price change for the N days after a timestamp. The 1 day forward return for AAPL on 2014-12-2 is the percent change in the AAPL open price on 2014-12-2 and the AAPL open price on 2014-12-3. The 5 day forward return is the percent change from open 2014-12-2 to open 2014-12-9 (5 trading days) divided by 5.
# Returns Analysis
Returns analysis gives us a raw description of a factor's value that shows us the power of a factor in real currency values.
One of the most basic ways to look at a factor's predicitve power is to look at the mean return of different factor quantile.
```python
mean_return_by_q_daily, std_err = alphalens.performance.mean_return_by_quantile(factor_data, by_date=True)
```
```python
mean_return_by_q_daily.head()
```
```python
mean_return_by_q, std_err_by_q = alphalens.performance.mean_return_by_quantile(factor_data, by_date=False)
```
```python
mean_return_by_q.head()
```
```python
alphalens.plotting.plot_quantile_returns_bar(mean_return_by_q);
```
By looking at the mean return by quantile we can get a real look at how well the factor differentiates forward returns across the signal values. Obviously we want securities with a better signal to exhibit higher returns. For a good factor we'd expect to see negative values in the lower quartiles and positive values in the upper quantiles.
```python
alphalens.plotting.plot_quantile_returns_violin(mean_return_by_q_daily);
```
This violin plot is similar to the one before it but shows more information about the underlying data. It gives a better idea about the range of values, the median, and the inter-quartile range. What gives the plots their shape is the application of a probability density of the data at different values.
```python
quant_return_spread, std_err_spread = alphalens.performance.compute_mean_returns_spread(mean_return_by_q_daily,
upper_quant=5,
lower_quant=1,
std_err=std_err)
```
```python
alphalens.plotting.plot_mean_quantile_returns_spread_time_series(quant_return_spread, std_err_spread);
```
This rolling forward returns spread graph allows us to look at the raw spread in basis points between the top and bottom quantiles over time. The green line is the returns spread while the orange line is a 1 month average to smooth the data and make it easier to visualize.
```python
alphalens.plotting.plot_cumulative_returns_by_quantile(mean_return_by_q_daily, period='1D')
```
By looking at the cumulative returns by factor quantile we can get an intuition for which quantiles are contributing the most to the factor and at what time. Ideally we would like to see a these curves originate at the same value on the left and spread out like a fan as they move to the right through time, with the higher quantiles on the top.
```python
ls_factor_returns = alphalens.performance.factor_returns(factor_data)
```
```python
ls_factor_returns.head()
```
```python
alphalens.plotting.plot_cumulative_returns(ls_factor_returns['1D'], period='1D')
```
While looking at quantiles is important we must also look at the factor returns as a whole. The cumulative factor long/short returns plot lets us view the combined effects overtime of our entire factor.
```python
alpha_beta = alphalens.performance.factor_alpha_beta(factor_data)
```
```python
alpha_beta
```
A very important part of factor returns analysis is determing the alpha, and how significant it is. Here we surface the annualized alpha, and beta.
## Returns Tear Sheet
We can view all returns analysis calculations together.
```python
alphalens.tears.create_returns_tear_sheet(factor_data)
```
# Information Analysis
Information Analysis is a way for us to evaluate the predicitive value of a factor without the confounding effects of transaction costs. The main way we look at this is through the Information Coefficient (IC).
From Wikipedia...
>The information coefficient (IC) is a measure of the merit of a predicted value. In finance, the information coefficient is used as a performance metric for the predictive skill of a financial analyst. The information coefficient is similar to correlation in that it can be seen to measure the linear relationship between two random variables, e.g. predicted stock returns and the actualized returns. The information coefficient ranges from 0 to 1, with 0 denoting no linear relationship between predictions and actual values (poor forecasting skills) and 1 denoting a perfect linear relationship (good forecasting skills).
```python
ic = alphalens.performance.factor_information_coefficient(factor_data)
```
```python
ic.head()
```
```python
alphalens.plotting.plot_ic_ts(ic);
```
By looking at the IC each day we can understand how theoretically predicitive our factor is overtime. We like our mean IC to be high and the standard deviation, or volatility of it, to be low. We want to find consistently predictive factors.
```python
alphalens.plotting.plot_ic_hist(ic);
```
Looking at a histogram of the daily IC values can indicate how the factor behaves most of the time, where the likely IC values will fall, it also allows us to see if the factor has fat tails.
```python
alphalens.plotting.plot_ic_qq(ic);
```
These Q-Q plots show the difference in shape between the distribution of IC values and a normal distribution. This is especially helpful in seeing how the most extreme values in the distribution affect the predicitive power.
```python
mean_monthly_ic = alphalens.performance.mean_information_coefficient(factor_data, by_time='M')
```
```python
mean_monthly_ic.head()
```
```python
alphalens.plotting.plot_monthly_ic_heatmap(mean_monthly_ic);
```
By displaying the IC data in heatmap format we can get an idea about the consistency of the factor, and how it behaves during different market regimes/seasons.
## Information Tear Sheet
We can view all information analysis calculations together.
```python
alphalens.tears.create_information_tear_sheet(factor_data)
```
# Turnover Analysis
Turnover Analysis gives us an idea about the nature of a factor's makeup and how it changes.
```python
quantile_factor = factor_data['factor_quantile']
turnover_period = 1
```
```python
quantile_turnover = pd.concat([alphalens.performance.quantile_turnover(quantile_factor, q, turnover_period)
for q in range(1, int(quantile_factor.max()) + 1)], axis=1)
```
```python
quantile_turnover.head()
```
```python
alphalens.plotting.plot_top_bottom_quantile_turnover(quantile_turnover, turnover_period)
```
Factor turnover is important as it indicates the incorporation of new information and the make up of the extremes of a signal. By looking at the new additions to the sets of top and bottom quantiles we can see how much of this factor is getting remade everyday.
```python
factor_autocorrelation = alphalens.performance.factor_rank_autocorrelation(factor_data, turnover_period)
```
```python
factor_autocorrelation.head()
```
```python
alphalens.plotting.plot_factor_rank_auto_correlation(factor_autocorrelation);
```
The autocorrelation of the factor indicates to us the persistence of the signal itself.
## Turnover Tear Sheet
We can view all turnover calculations together.
```python
alphalens.tears.create_turnover_tear_sheet(factor_data)
```
# Event Style Returns Analysis
Looking at the average cumulative return in a window before and after a factor can indicate to us how long the predicative power of a factor lasts. This tear sheet takes a while to run.
**NOTE:** This tear sheet takes in an extra argument `pricing`.
```python
alphalens.tears.create_event_returns_tear_sheet(factor_data, pricing, by_group=True)
```
# Groupwise
Many of the plots in Alphalens can be viewed on their own by grouping if grouping information is provided. The returns and information tear sheets can be viewed groupwise by passing in the `by_group=True` argument.
```python
ic_by_sector = alphalens.performance.mean_information_coefficient(factor_data, by_group=True)
```
```python
ic_by_sector.head()
```
```python
alphalens.plotting.plot_ic_by_group(ic_by_sector);
```
```python
mean_return_quantile_sector, mean_return_quantile_sector_err = alphalens.performance.mean_return_by_quantile(factor_data, by_group=True)
```
```python
mean_return_quantile_sector.head()
```
```python
alphalens.plotting.plot_quantile_returns_bar(mean_return_quantile_sector, by_group=True);
```
# Summary Tear Sheet
There are a lot of plots above. If you want a quick snapshot of how the alpha factor performs consider the summary tear sheet.
```python
alphalens.tears.create_summary_tear_sheet(factor_data)
```
# The Whole Thing
If you want to see all of the results create a full tear sheet. By passing in the factor data you can analyze all of the above statistics and plots at once.
```python
alphalens.tears.create_full_tear_sheet(factor_data)
```
```python
```























Shown in full with attribution under the source's licence. Licence: Apache-2.0
This summary was written by Stratmill's research agent from the original; it is not a copy of the source.