Using the Fama-French factors in Python to test a portfolio’s alpha
When you are building portfolios, backtesting, and analyzing performance, you need to test whether a portfolio’s performance is due to skill or to something that systematically varies with a set of factor exposures. It is not that difficult to construct a portfolio that has performed better than the market if you are building a momentum portfolio or a value portfolio. But that is because you have exposure to other factors, and when you account for them, much of the alpha disappears.
In this post, I will show you how to quickly get the set of factors normally used to evaluate a portfolio’s alpha and test the portfolio.
Step 1. Downloading the factors
I download the five Fama-French factors from Kenneth French’s website, because it processes, collects, and stores the factors’ returns. That makes it easy to get them and test them directly, without constructing the factors yourself. Anyone interested can view my other post on how he constructs them.
import io
import urllib.request
import zipfile
from itertools import takewhile
import matplotlib.pyplot as plt
import pandas as pd
import statsmodels.api as sm
LIBRARY = "https://mba.tuck.dartmouth.edu/pages/faculty/ken.french/ftp/"
def download(name):
with urllib.request.urlopen(LIBRARY + name) as response:
archive = zipfile.ZipFile(io.BytesIO(response.read()))
return archive.read(archive.namelist()[0]).decode("latin-1")
def monthly_table(text):
lines = text.splitlines()
start = next(number for number, line in enumerate(lines) if line.startswith(","))
rows = list(takewhile(str.strip, lines[start:]))
table = pd.read_csv(io.StringIO("\n".join(rows)), index_col=0)
table.index = pd.PeriodIndex(table.index.astype(str).str.strip(), freq="M")
table.columns = table.columns.str.strip()
return table.replace([-99.99, -999], float("nan"))
factors = monthly_table(download("F-F_Research_Data_5_Factors_2x3_CSV.zip"))
print(f"{factors.index[0]} to {factors.index[-1]}, {len(factors)} months")
print(factors.tail(3).to_string())1963-07 to 2026-09, 759 months
Mkt-RF SMB HML RMW CMA RF
2026-07 -0.61 -0.09 2.11 11.07 2.27 0.33
2026-08 2.56 -0.52 -3.54 -4.09 -2.01 0.29
2026-09 -1.07 -4.60 -1.55 -3.22 1.08 0.31
Step 2. Building the portfolio
Kenneth French uses each company’s industry code to identify which stocks belong to which industry. I then use three of these industries, tobacco, alcohol, and defense, to form an equal-weighted portfolio, and I keep these three fixed throughout the whole sample. Even though I am equally weighting across the three industries, the stocks within each industry are weighted by market value.
I chose industries because a portfolio built on industries has no clear association with the factors. If I did a momentum strategy, it would probably have a high exposure to the momentum factor, but with industries, it is not that clear-cut. So I am just taking three industries to keep things simple.
The table shows the portfolio’s excess return in the last month of the sample.
| September 2026 | Return |
|---|---|
| Tobacco | 1.21% |
| Alcohol | −8.03% |
| Defense | −8.91% |
| Portfolio, the average of the three | −5.24% |
| T-bill | 0.31% |
| Excess return | −5.55% |
industries = monthly_table(download("49_Industry_Portfolios_CSV.zip"))
portfolio = (industries["Smoke"] + industries["Beer"] + industries["Guns"]) / 3
data = factors.join(portfolio.rename("portfolio"), how="inner").dropna()
data["excess"] = data["portfolio"] - data["RF"]
market = data["Mkt-RF"] + data["RF"]
print(f"{data.index[0]} to {data.index[-1]}, {len(data)} months")
print(f"Portfolio: {12 * data['portfolio'].mean():.2f}% a year")
print(f"Market: {12 * market.mean():.2f}% a year")1963-07 to 2026-09, 759 months
Portfolio: 14.27% a year
Market: 11.55% a year
The portfolio returned 14.27% a year from July 1963 to September 2026, against 11.55% for the market.
Step 3. Running the regression
Below is the formula used to test a portfolio’s alpha. I have the excess return, so the return of the portfolio in excess of the T-bills or the risk-free rate, and a set of factors that stocks tend to vary with if they have exposure to these factors systematically. I am choosing the five factors: the market (Mkt-RF), size (small minus big, SMB), value (high minus low, HML), profitability (robust minus weak, RMW), and investment (conservative minus aggressive, CMA).
\[ \begin{aligned} \text{excess}_t &= \alpha + \beta_1 \text{Mkt-RF}_t + \beta_2 \text{SMB}_t + \beta_3 \text{HML}_t\\ & \quad + \beta_4 \text{RMW}_t + \beta_5 \text{CMA}_t + e_t \end{aligned} \]
Alpha is the intercept: the average return the factors cannot explain. So if that is significant and positive, it means the portfolio has something the factors cannot explain, such as the manager being highly skilled at picking winning stocks. I use Newey-West standard errors to adjust for the fact that the residuals may change in size and could be correlated over time. The returns are monthly, so I multiply the alpha by 12 to state it in points a year.
models = {"Market": ["Mkt-RF"],
"Three factors": ["Mkt-RF", "SMB", "HML"],
"Five factors": ["Mkt-RF", "SMB", "HML", "RMW", "CMA"]}
fits = {name: sm.OLS(data["excess"], sm.add_constant(data[columns])).fit(
cov_type="HAC", cov_kwds={"maxlags": 6})
for name, columns in models.items()}
alphas = pd.DataFrame({"alpha a year": [12 * fit.params["const"] for fit in fits.values()],
"t-statistic": [fit.tvalues["const"] for fit in fits.values()]},
index=fits.keys())
loadings = pd.DataFrame({"loading": fits["Five factors"].params.drop("const"),
"t-statistic": fits["Five factors"].tvalues.drop("const")})
print(alphas.round(2).to_string())
print()
print(loadings.round(2).to_string()) alpha a year t-statistic
Market 4.72 2.86
Three factors 3.59 2.32
Five factors 0.44 0.34
loading t-statistic
Mkt-RF 0.84 25.66
SMB 0.11 2.15
HML 0.02 0.44
RMW 0.62 8.93
CMA 0.44 4.89
Step 4. Reading the alpha
t-statistic above 1.96
t-statistic below 1.96
The main point is that when I include only the market factor, I have an alpha of 4.72 points a year, with a t-statistic of 2.86, which is significant because it is above 1.96. So I can conclude that there is an alpha, something that the market factor cannot explain for the industry portfolio. When I include the three factors, it is still there, at 3.59 points a year with a t-statistic of 2.32. But when I include the five factors, the alpha disappears, at 0.44 points a year with a t-statistic of 0.34, which is below 1.96. The industry portfolio has exposure to these factors, and the alpha I first thought it had is more or less just exposure to the other factors.
Viewing the loadings, I can see that the portfolio has a strong association with profitability and investment, at 0.62 and 0.44. So stocks in the industry portfolio behave much like companies that are profitable and invest little.
BG, INK, GRID, SPINE = "#FCEFE3", "#1f1f1f", "#EADCCC", "#D5C6B4"
colors = ["#17868A" if abs(t) > 1.96 else "#D2822B" for t in alphas["t-statistic"]]
labels = [f"{alpha:.2f}\n(t = {t:.2f})" for alpha, t in alphas.to_numpy()]
fig, axis = plt.subplots(figsize=(6.4, 4.4))
bars = axis.bar(alphas.index, alphas["alpha a year"], color=colors, width=0.6)
axis.bar_label(bars, labels=labels, padding=4, color=INK)
axis.set_ylabel("Alpha, points a year", color=INK)
axis.set_ylim(0, 6)
fig.patch.set_facecolor(BG)
axis.set_facecolor(BG)
axis.grid(True, axis="y", color=GRID, lw=1.0)
axis.set_axisbelow(True)
axis.spines[["top", "right"]].set_visible(False)
axis.spines[["left", "bottom"]].set_color(SPINE)
axis.tick_params(axis="both", length=0, colors=INK)
fig.savefig("fama_french_alpha.png", dpi=200, bbox_inches="tight")Limitations
The limitation is that all the factors are before trading costs, so your portfolio’s returns should also be before costs. But that is a problem because one way to bypass this test is to have a portfolio with high returns that can be due to high turnover, and thus with high trading costs. The net return may then be low, but you will not see that in this type of regression. One published example, from Novy-Marx and Velikov (2016), is a strategy called industry relative reversals. Before costs, it returns 0.98% a month and has an alpha of 1.05% a month against four factors, with a t-statistic of 6.66. It turns over about 90% of its positions a month and costs 1.78% a month to trade, so its net return is -0.80% a month, which is essentially a losing portfolio.
All the returns are also in US dollars. So if your portfolio is in another currency, you need to adjust for that before you run the regression, for example by converting your portfolio’s returns to US dollars, because otherwise the currency’s movements affect the alpha and the loadings.
Conclusion
This post is a tutorial on quickly getting Kenneth French’s factors in Python and testing whether a portfolio has an edge that the factors cannot explain. Also, always consider whether the portfolio is executable, whether costs are too high, what the trading costs are, what the net return is in a more realistic backtest, what the price impact is, and what the possible slippage is.
Read next
- How to use Kenneth French’s data library
- Testing a stock characteristic as a factor
- Trading costs more than halved Swedish momentum’s ending value
Disclaimer: a teaching example, not investment advice.
Data and sources: Kenneth R. French, Data Library, Fama/French 5 Factors (2x3) and 49 Industry Portfolios, downloaded on 2 November 2026 (built from the Center for Research in Security Prices (CRSP) database of September 2026). Eugene F. Fama and Kenneth R. French, “A five-factor asset pricing model”, Journal of Financial Economics (2015). Robert Novy-Marx and Mihail Velikov, “A taxonomy of anomalies and their trading costs”, Review of Financial Studies (2016), for industry relative reversals. Andrew Detzel, Robert Novy-Marx and Mihail Velikov, “Model comparison with transaction costs”, Journal of Finance (2023).