What is the purpose of this case study?
This case study demonstrates how to apply the Fama-French 5-factor model (Fama & French, 2015) to test whether a broad equity index's returns can be explained by exposures to five common risk factors: market excess return, size, value, profitability, and investment pattern. The case study uses the Shiller S&P Composite dataset (1871-present) as the asset to be explained, and the Kenneth French Data Library 5-factor dataset (1963-present) as the explanatory factors.
The case study is a worked example, not a new research finding. The expected results that a market-cap-weighted index is well-explained by the five factors, with an alpha close to zero and a high R-squared are documented in Fama & French (2015) and are reproduced here using public data and standard statistical software. The honest practitioner treats the expected findings as a sanity check, not as a guaranteed result; real-world data may differ from the published literature due to sample period, data cleaning, or implementation details.
The case study is part of a series of reproducibility assets that TheQuantHackers publishes to demonstrate the application of academic quantitative finance methodology to public datasets. The previous case study in the series applied the López de Prado CPCV + deflated Sharpe framework to a 12-month price-momentum strategy on the same Shiller dataset (see /methodology/backtest-audit).
What data is required?
Two datasets are required. The first is the Shiller S&P Composite dataset, which provides monthly real price, dividend, earnings, and CPI data for the S&P Composite index (and its pre-1928 predecessors) from 1871 to the present. The data is freely available from Robert Shiller's Yale page and is free for academic and personal use with attribution. The data file is a single Excel or CSV file with monthly observations.
The second dataset is the Fama-French 5-Factor dataset, which provides monthly returns for the five factors (Mkt-RF, SMB, HML, RMW, CMA) and the risk-free rate (RF) from 1963 to the present. The data is freely available from Kenneth French's Data Library at Dartmouth and is free for academic and personal use with attribution. The data file is a single CSV file with monthly observations.
The case study restricts the analysis to the period 1963-present, where both datasets are available. This is a standard restriction in the factor-model literature and is necessary because the Fama-French factors are not available before 1963. The full Shiller dataset (1871-1963) is useful for other analyses but is not used in this case study.
How is the regression model specified?
The regression model is a standard OLS regression of the asset's excess return on the five factors. The dependent variable is the monthly excess return of the Shiller S&P Composite (the total return minus the risk-free rate). The independent variables are the five Fama-French factors: Mkt-RF (market excess return), SMB (small minus big), HML (high minus low), RMW (robust minus weak), and CMA (conservative minus aggressive).
The regression includes a constant term (alpha) which captures the average excess return of the asset that is not explained by the five factors. Under the null hypothesis that the asset is well-explained by the five factors, the alpha should be statistically indistinguishable from zero. The regression is estimated via OLS with Newey-West standard errors to account for heteroskedasticity and autocorrelation in the monthly returns (Newey & West, 1987).
Python OLS regression specificationimport pandas as pd import statsmodels.api as sm # Load data shiller = pd.read_csv("shiller_data.csv", parse_dates=["Date"]) ff5 = pd.read_csv("ff5_monthly.csv", parse_dates=["Date"]) # Merge on date df = pd.merge(shiller, ff5, on="Date", how="inner") # Compute excess return of the S&P Composite df["SP_Return"] = df["SP_Price"].pct_change() df["SP_Excess"] = df["SP_Return"] - df["RF"] # Define regression variables y = df["SP_Excess"].dropna() X = df[["Mkt-RF", "SMB", "HML", "RMW", "CMA"]].dropna() X = sm.add_constant(X) # Align indices y, X = y.align(X, join="inner") # OLS regression with Newey-West standard errors model = sm.OLS(y, X).fit( cov_type="HAC", cov_kwds={"maxlags": 12} ) print(model.summary())
How is the model interpreted?
The regression output provides three key pieces of information. First, the alpha (intercept) indicates whether the asset has a risk-adjusted excess return that is not explained by the five factors. For a market-cap-weighted index, the alpha should be small and statistically insignificant, indicating that the index's return is fully explained by its factor exposures. A significant positive alpha would indicate that the index has some 'hidden' return that is not captured by the five factors, which would be a surprising finding for a broad market index.
Second, the factor loadings (betas) indicate the asset's exposure to each of the five factors. A market-cap-weighted index should have a market loading (Mkt-RF beta) close to 1.0, as expected. The other loadings (SMB, HML, RMW, CMA) should be small or zero, indicating that the index is not tilted toward small-cap, value, profitability, or investment-pattern factors.
Third, the R-squared indicates the proportion of the asset's return variation that is explained by the five factors. A high R-squared (above 0.90) indicates that the factors are a good model for the asset's returns. A low R-squared would indicate that the five factors are insufficient to explain the asset's returns, and additional factors or a different model specification may be needed.
How is the model's stability tested?
The model's stability is tested by running the regression on sub-periods of the data and comparing the factor loadings and R-squared across sub-periods. A stable model should have similar factor loadings and R-squared across sub-periods, indicating that the model is robust to changes in the market environment.
A common approach is to split the data into two equal sub-periods (e.g., 1963-1993 and 1993-present) and run the regression on each sub-period. If the factor loadings and R-squared are similar across the two sub-periods, the model is considered stable. If they differ substantially, the model may be overfitting to a specific market regime or may not be robust to changes in the market environment.
Python Sub-period stability testimport pandas as pd import statsmodels.api as sm # Load and merge data (same as before) shiller = pd.read_csv("shiller_data.csv", parse_dates=["Date"]) ff5 = pd.read_csv("ff5_monthly.csv", parse_dates=["Date"]) df = pd.merge(shiller, ff5, on="Date", how="inner") df["SP_Return"] = df["SP_Price"].pct_change() df["SP_Excess"] = df["SP_Return"] - df["RF"] # Split into two sub-periods midpoint = df["Date"].quantile(0.5) df_1 = df[df["Date"] < midpoint] df_2 = df[df["Date"] >= midpoint] # Run regression on each sub-period def run_regression(df_sub): y = df_sub["SP_Excess"].dropna() X = df_sub[["Mkt-RF", "SMB", "HML", "RMW", "CMA"]].dropna() X = sm.add_constant(X) y, X = y.align(X, join="inner") return sm.OLS(y, X).fit(cov_type="HAC", cov_kwds={"maxlags": 12}) results_1 = run_regression(df_1) results_2 = run_regression(df_2) print("=== Sub-period 1 ===") print(results_1.summary()) print("=== Sub-period 2 ===") print(results_2.summary())
What are the limitations of this case study?
The case study has several limitations. First, the Shiller S&P Composite is used as a proxy for a market-cap-weighted equity index, but it is actually a price-weighted index of the S&P 500 (and its predecessors). The price-weighting is a simplification, and a true market-cap-weighted index would have slightly different factor loadings. The case study uses the S&P Composite for consistency with the Shiller dataset and the backtest audit case study.
Second, the case study restricts the analysis to the period 1963-present because the Fama-French factors are not available before 1963. This means that the case study does not test the model's explanatory power during the pre-1963 period, which includes several major market events (e.g., the 1929 crash, the Great Depression, World War II). A full audit of the model's robustness would require testing on the pre-1963 period as well, but this is not possible with the current data.
Third, the case study uses a simple OLS regression without controlling for time-varying factor loadings or regime changes. A more sophisticated analysis would use a rolling-window regression or a Kalman filter to estimate time-varying loadings, which would provide a more nuanced view of the model's stability. The case study is a worked example, not a comprehensive analysis, and the honest practitioner would extend the analysis with additional diagnostics as needed.
How does this case study relate to the published literature?
This case study is a direct application of the methodology in Fama & French (2015), A Five-Factor Asset Pricing Model. The paper proposes the five-factor model as an extension of the original three-factor model (Fama & French, 1993), with the addition of profitability (RMW) and investment pattern (CMA) factors. The paper documents that the five-factor model explains 90-95% of the variation in monthly returns for a broad set of US equities, and that the model's explanatory power is robust across different sample periods and portfolio sorts.
The case study's expected findings alpha close to zero, R-squared above 0.90, and stable factor loadings across sub-periods are consistent with the paper's main results. The case study is not a new research finding; it is a worked example that demonstrates the methodology on public data. The honest practitioner treats the case study as a sanity check, not as a guaranteed result.
How can the case study be extended?
The case study can be extended in several ways. First, the regression can be run on a cross-section of assets (e.g., the 100 largest US equities) to test whether the five-factor model explains the cross-section of returns. This is the standard test in the asset pricing literature and is documented in Fama & French (2015, Section 4).
Second, the case study can be extended to include additional factors (e.g., momentum, liquidity, betting against beta) to test whether the five-factor model is sufficient or whether additional factors are needed. The q-factor model (Hou, Xue & Zhang, 2015) and the mispricing factor model (Stambaugh & Yuan, 2017) are two prominent extensions.
Third, the case study can be extended to international markets (e.g., European, Asian, emerging markets) to test whether the five-factor model is a universal model or whether it is specific to the US market. The international evidence is mixed: the five-factor model works well in developed markets but has lower explanatory power in emerging markets (e.g., Fama & French, 2017).
Python Full reproducibility script""" Fama-French 5-Factor Audit Full reproducibility script. Run this script with the Shiller and Fama-French data files in the current directory. Expected output: OLS regression summaries and sub-period stability test results. """ import pandas as pd import statsmodels.api as sm def load_data(): shiller = pd.read_csv("shiller_data.csv", parse_dates=["Date"]) ff5 = pd.read_csv("ff5_monthly.csv", parse_dates=["Date"]) return pd.merge(shiller, ff5, on="Date", how="inner") def prepare_returns(df): df = df.copy() df["SP_Return"] = df["SP_Price"].pct_change() df["SP_Excess"] = df["SP_Return"] - df["RF"] return df.dropna() def run_ols(df): y = df["SP_Excess"] X = sm.add_constant(df[["Mkt-RF", "SMB", "HML", "RMW", "CMA"]]) return sm.OLS(y, X).fit(cov_type="HAC", cov_kwds={"maxlags": 12}) def main(): df = prepare_returns(load_data()) print(f"Observations: {len(df)}") print(f"Period: {df['Date'].min()} to {df['Date'].max()}\n") print("=== Full-period regression ===") print(run_ols(df).summary()) midpoint = df["Date"].quantile(0.5) print(f"\n=== Sub-period 1 (before {midpoint}) ===") print(run_ols(df[df["Date"] < midpoint]).summary()) print(f"\n=== Sub-period 2 (from {midpoint}) ===") print(run_ols(df[df["Date"] >= midpoint]).summary()) if __name__ == "__main__": main()