Chapter 3 · Section 9 practice
Extension: Financial Factors and Model Comparison
Work from question 1 to 10. Edit your Python box, click Run Code, review the result, then click Submit attempt. Reference answers unlock after all ten attempts are submitted. This records completion, not correctness. Each box runs independently. Progress is saved in this browser.
1. Read factor names
Warm-up. Print the five predictor names.
Write your answer, then run it.
2. Read market loading
Warm-up. Print the Mkt-RF coefficient.
Write your answer, then run it.
3. Read alpha uncertainty
Warm-up. Print the 95% interval for const.
Write your answer, then run it.
4. Read adjusted fit
Build your skills. Print adjusted R-squared.
Write your answer, then run it.
5. Choose a search criterion
Build your skills. Given three candidate models, print the best by adjusted R-squared and the best by RMSE s.
Write your answer, then run it.
6. Fit a nested comparison
Build your skills. Fit a market-factor-only model on train and print its adjusted R-squared beside the five-factor value.
Write your answer, then run it.
7. Compare AIC
Build your skills. Print the market-only AIC and five-factor AIC.
Write your answer, then run it.
8. Evaluate five factors later
Challenge. Print five-factor test adjusted R-squared using the course formula, with k=5 and n equal to the test sample size.
Write your answer, then run it.
9. Calculate a factor VIF
Challenge. Regress HML on the other four factors and reconstruct HML VIF.
Write your answer, then run it.
10. Simplify the PPT factor example
Challenge. In the PPT illustration, CMA has p=0.18; all other slope p-values are below 0.05. Print a predictor list without CMA, and explain what to do next.
Write your answer, then run it.
Reference answers are locked until all 10 attempts are submitted.
1. Read factor names
import pandas as pd
prices = pd.read_csv(
"https://busanalytics-book.pages.dev/data/nvda_spy_daily_2023_2024.csv",
index_col="Date", parse_dates=True).sort_index()
factors = pd.read_csv(
"https://busanalytics-book.pages.dev/data/ff5_daily_2023_2024.csv",
index_col="Date", parse_dates=True) / 100
returns = prices.pct_change(fill_method=None)
data = returns.copy()
# Series columns match the Date index, not the row position.
data["Mkt-RF"] = factors["Mkt-RF"]
data["SMB"] = factors["SMB"]
data["HML"] = factors["HML"]
data["RMW"] = factors["RMW"]
data["CMA"] = factors["CMA"]
data["RF"] = factors["RF"]
# Keep complete dates for all model comparisons.
data = data.dropna()
data["Excess"] = data["NVDA"] - data["RF"]
data["Market"] = data["SPY"] - data["RF"]
cut = int(len(data) * 0.8)
train = data.iloc[:cut].copy()
test = data.iloc[cut:].copy()
import statsmodels.api as sm
features = ["Mkt-RF", "SMB", "HML", "RMW", "CMA"]
X = sm.add_constant(train[features])
model = sm.OLS(train["Excess"], X).fit()
print(features)Expected output
['Mkt-RF', 'SMB', 'HML', 'RMW', 'CMA']They are factor returns, not company accounting attributes.
2. Read market loading
import pandas as pd
prices = pd.read_csv(
"https://busanalytics-book.pages.dev/data/nvda_spy_daily_2023_2024.csv",
index_col="Date", parse_dates=True).sort_index()
factors = pd.read_csv(
"https://busanalytics-book.pages.dev/data/ff5_daily_2023_2024.csv",
index_col="Date", parse_dates=True) / 100
returns = prices.pct_change(fill_method=None)
data = returns.copy()
# Series columns match the Date index, not the row position.
data["Mkt-RF"] = factors["Mkt-RF"]
data["SMB"] = factors["SMB"]
data["HML"] = factors["HML"]
data["RMW"] = factors["RMW"]
data["CMA"] = factors["CMA"]
data["RF"] = factors["RF"]
# Keep complete dates for all model comparisons.
data = data.dropna()
data["Excess"] = data["NVDA"] - data["RF"]
data["Market"] = data["SPY"] - data["RF"]
cut = int(len(data) * 0.8)
train = data.iloc[:cut].copy()
test = data.iloc[cut:].copy()
import statsmodels.api as sm
features = ["Mkt-RF", "SMB", "HML", "RMW", "CMA"]
X = sm.add_constant(train[features])
model = sm.OLS(train["Excess"], X).fit()
print(round(model.params["Mkt-RF"], 4))Expected output
2.0847The model uses the factor market definition, not SPY−RF.
3. Read alpha uncertainty
import pandas as pd
prices = pd.read_csv(
"https://busanalytics-book.pages.dev/data/nvda_spy_daily_2023_2024.csv",
index_col="Date", parse_dates=True).sort_index()
factors = pd.read_csv(
"https://busanalytics-book.pages.dev/data/ff5_daily_2023_2024.csv",
index_col="Date", parse_dates=True) / 100
returns = prices.pct_change(fill_method=None)
data = returns.copy()
# Series columns match the Date index, not the row position.
data["Mkt-RF"] = factors["Mkt-RF"]
data["SMB"] = factors["SMB"]
data["HML"] = factors["HML"]
data["RMW"] = factors["RMW"]
data["CMA"] = factors["CMA"]
data["RF"] = factors["RF"]
# Keep complete dates for all model comparisons.
data = data.dropna()
data["Excess"] = data["NVDA"] - data["RF"]
data["Market"] = data["SPY"] - data["RF"]
cut = int(len(data) * 0.8)
train = data.iloc[:cut].copy()
test = data.iloc[cut:].copy()
import statsmodels.api as sm
features = ["Mkt-RF", "SMB", "HML", "RMW", "CMA"]
X = sm.add_constant(train[features])
model = sm.OLS(train["Excess"], X).fit()
print(model.conf_int().loc["const"].round(6).to_list())Expected output
[-5.4e-05, 0.00459]A nonzero estimate alone does not establish statistically significant alpha.
4. Read adjusted fit
import pandas as pd
prices = pd.read_csv(
"https://busanalytics-book.pages.dev/data/nvda_spy_daily_2023_2024.csv",
index_col="Date", parse_dates=True).sort_index()
factors = pd.read_csv(
"https://busanalytics-book.pages.dev/data/ff5_daily_2023_2024.csv",
index_col="Date", parse_dates=True) / 100
returns = prices.pct_change(fill_method=None)
data = returns.copy()
# Series columns match the Date index, not the row position.
data["Mkt-RF"] = factors["Mkt-RF"]
data["SMB"] = factors["SMB"]
data["HML"] = factors["HML"]
data["RMW"] = factors["RMW"]
data["CMA"] = factors["CMA"]
data["RF"] = factors["RF"]
# Keep complete dates for all model comparisons.
data = data.dropna()
data["Excess"] = data["NVDA"] - data["RF"]
data["Market"] = data["SPY"] - data["RF"]
cut = int(len(data) * 0.8)
train = data.iloc[:cut].copy()
test = data.iloc[cut:].copy()
import statsmodels.api as sm
features = ["Mkt-RF", "SMB", "HML", "RMW", "CMA"]
X = sm.add_constant(train[features])
model = sm.OLS(train["Excess"], X).fit()
print(round(model.rsquared_adj, 4))Expected output
0.4879This is a penalised training fit measure.
5. Choose a search criterion
import pandas as pd
candidates = pd.DataFrame(
{"AdjR2": [0.40, 0.50, 0.48], "RMSE": [6**0.5, 5**0.5, 5.2**0.5]},
index=["A", "B", "C"],
)
print(candidates["AdjR2"].idxmax())
print(candidates["RMSE"].idxmin())Expected output
B
BOn the same fitting rows and outcome, adjusted R-squared and RMSE s give the same preference: model B. The search procedure enumerates candidates; the criterion scores them.
6. Fit a nested comparison
import pandas as pd
prices = pd.read_csv(
"https://busanalytics-book.pages.dev/data/nvda_spy_daily_2023_2024.csv",
index_col="Date", parse_dates=True).sort_index()
factors = pd.read_csv(
"https://busanalytics-book.pages.dev/data/ff5_daily_2023_2024.csv",
index_col="Date", parse_dates=True) / 100
returns = prices.pct_change(fill_method=None)
data = returns.copy()
# Series columns match the Date index, not the row position.
data["Mkt-RF"] = factors["Mkt-RF"]
data["SMB"] = factors["SMB"]
data["HML"] = factors["HML"]
data["RMW"] = factors["RMW"]
data["CMA"] = factors["CMA"]
data["RF"] = factors["RF"]
# Keep complete dates for all model comparisons.
data = data.dropna()
data["Excess"] = data["NVDA"] - data["RF"]
data["Market"] = data["SPY"] - data["RF"]
cut = int(len(data) * 0.8)
train = data.iloc[:cut].copy()
test = data.iloc[cut:].copy()
import statsmodels.api as sm
features = ["Mkt-RF", "SMB", "HML", "RMW", "CMA"]
X = sm.add_constant(train[features])
model = sm.OLS(train["Excess"], X).fit()
one = sm.OLS(train["Excess"], sm.add_constant(train[["Mkt-RF"]])).fit()
print(round(one.rsquared_adj, 4), round(model.rsquared_adj, 4))Expected output
0.3141 0.4879Compare adjusted R-squared because the models have k=1 and k=5 predictors, fitted on identical rows.
7. Compare AIC
import pandas as pd
prices = pd.read_csv(
"https://busanalytics-book.pages.dev/data/nvda_spy_daily_2023_2024.csv",
index_col="Date", parse_dates=True).sort_index()
factors = pd.read_csv(
"https://busanalytics-book.pages.dev/data/ff5_daily_2023_2024.csv",
index_col="Date", parse_dates=True) / 100
returns = prices.pct_change(fill_method=None)
data = returns.copy()
# Series columns match the Date index, not the row position.
data["Mkt-RF"] = factors["Mkt-RF"]
data["SMB"] = factors["SMB"]
data["HML"] = factors["HML"]
data["RMW"] = factors["RMW"]
data["CMA"] = factors["CMA"]
data["RF"] = factors["RF"]
# Keep complete dates for all model comparisons.
data = data.dropna()
data["Excess"] = data["NVDA"] - data["RF"]
data["Market"] = data["SPY"] - data["RF"]
cut = int(len(data) * 0.8)
train = data.iloc[:cut].copy()
test = data.iloc[cut:].copy()
import statsmodels.api as sm
features = ["Mkt-RF", "SMB", "HML", "RMW", "CMA"]
X = sm.add_constant(train[features])
model = sm.OLS(train["Excess"], X).fit()
one = sm.OLS(train["Excess"], sm.add_constant(train[["Mkt-RF"]])).fit()
print(round(one.aic, 3), round(model.aic, 3))Expected output
-1752.459 -1865.346Lower AIC is preferred by this in-sample criterion; it need not win every criterion.
8. Evaluate five factors later
import pandas as pd
prices = pd.read_csv(
"https://busanalytics-book.pages.dev/data/nvda_spy_daily_2023_2024.csv",
index_col="Date", parse_dates=True).sort_index()
factors = pd.read_csv(
"https://busanalytics-book.pages.dev/data/ff5_daily_2023_2024.csv",
index_col="Date", parse_dates=True) / 100
returns = prices.pct_change(fill_method=None)
data = returns.copy()
# Series columns match the Date index, not the row position.
data["Mkt-RF"] = factors["Mkt-RF"]
data["SMB"] = factors["SMB"]
data["HML"] = factors["HML"]
data["RMW"] = factors["RMW"]
data["CMA"] = factors["CMA"]
data["RF"] = factors["RF"]
# Keep complete dates for all model comparisons.
data = data.dropna()
data["Excess"] = data["NVDA"] - data["RF"]
data["Market"] = data["SPY"] - data["RF"]
cut = int(len(data) * 0.8)
train = data.iloc[:cut].copy()
test = data.iloc[cut:].copy()
import statsmodels.api as sm
features = ["Mkt-RF", "SMB", "HML", "RMW", "CMA"]
X = sm.add_constant(train[features])
model = sm.OLS(train["Excess"], X).fit()
p = model.predict(sm.add_constant(test[features]))
y = test["Excess"]
n, k = len(test), 5
sse = ((y - p) ** 2).sum()
sst = ((y - y.mean()) ** 2).sum()
adjusted = 1 - (sse / (n - k - 1)) / (sst / (n - 1))
print(round(adjusted, 4))Expected output
0.2245Use the frozen training coefficients on the same later dates; do not refit the test outcomes.
9. Calculate a factor VIF
import pandas as pd
prices = pd.read_csv(
"https://busanalytics-book.pages.dev/data/nvda_spy_daily_2023_2024.csv",
index_col="Date", parse_dates=True).sort_index()
factors = pd.read_csv(
"https://busanalytics-book.pages.dev/data/ff5_daily_2023_2024.csv",
index_col="Date", parse_dates=True) / 100
returns = prices.pct_change(fill_method=None)
data = returns.copy()
# Series columns match the Date index, not the row position.
data["Mkt-RF"] = factors["Mkt-RF"]
data["SMB"] = factors["SMB"]
data["HML"] = factors["HML"]
data["RMW"] = factors["RMW"]
data["CMA"] = factors["CMA"]
data["RF"] = factors["RF"]
# Keep complete dates for all model comparisons.
data = data.dropna()
data["Excess"] = data["NVDA"] - data["RF"]
data["Market"] = data["SPY"] - data["RF"]
cut = int(len(data) * 0.8)
train = data.iloc[:cut].copy()
test = data.iloc[cut:].copy()
import statsmodels.api as sm
features = ["Mkt-RF", "SMB", "HML", "RMW", "CMA"]
X = sm.add_constant(train[features])
model = sm.OLS(train["Excess"], X).fit()
others = ["Mkt-RF", "SMB", "RMW", "CMA"]
aux = sm.OLS(train["HML"], sm.add_constant(train[others])).fit()
print(round(1 / (1 - aux.rsquared), 3))Expected output
1.922The auxiliary response is HML, not NVDA excess return.
10. Simplify the PPT factor example
features = ["Mkt-RF", "SMB", "HML", "RMW", "CMA"]
four_factors = ["Mkt-RF", "SMB", "HML", "RMW"]
print(four_factors)
print(
"Keep the intercept, refit, and compare adjusted R-squared and RMSE s; check validation R²."
)Expected output
['Mkt-RF', 'SMB', 'HML', 'RMW']
Keep the intercept, refit, and compare adjusted R-squared and RMSE s; check validation R².Read the p-value table and explicitly edit the predictor list. No elimination algorithm is required; a large p-value does not prove no business value.