Introduction to Business Analytics — HKUST
Why it matters
Prediction = data-driven decisions.
Instead of guessing, we let the data tell us what to expect.
Today’s finance questions:
Visual idea: CAPM fits a line through NVDA returns vs. market returns — slope = beta, intercept = alpha.
The Pearson correlation \(r\) measures the strength of linear association:
\[ r = \frac{\sum_{i=1}^{n}(x_i - \bar{x})(y_i - \bar{y})}{\sqrt{\sum_{i=1}^{n}(x_i - \bar{x})^2 \cdot \sum_{i=1}^{n}(y_i - \bar{y})^2}} \]
| \(r\) value | Interpretation |
|---|---|
| \(-1\) | Perfect negative linear |
| \(-0.5\) | Strong negative |
| \(0\) | No linear association |
| \(+0.5\) | Strong positive |
| \(+1\) | Perfect positive linear |
Key takeaway
\(r\) ranges from \(-1\) to \(+1\). Close to \(\pm 1\) = strong linear relationship. Close to \(0\) = weak or no linear relationship.
In the real world you’d use yfinance to pull live prices. For these slides we pre-fetched real NVDA and SPY daily closes for 2023–2024 and ship them as a CSV — so the analysis runs in your browser, with real data, instantly.
What just happened
The browser fetched a small CSV (~25 KB) from this site, parsed it into a pandas DataFrame, and computed daily returns + correlation — all in your browser. No yfinance, no Colab, no Binder, no tab switching.
For Topic 3 demos that need live data
The same CSV-pre-fetch pattern is used throughout this topic. Production code (for an assignment, your own analysis) should still use yfinance — run it in Colab for live prices.
For comparison — Binder lets you run a real Jupyter kernel inline with any package (yfinance, isom2600, etc.) but takes 30s–2min to cold-start.
How Binder works:
requirements.txt from a public GitHub repoBest for: occasional one-off demos that need packages Pyodide doesn’t have.
Not for: every-day teaching — the wait kills lecture flow.
First load ~30s–2min. Or open in new tab.
Key takeaway
Slope \(>1\): NVDA moves more than the market.
\[ y = \alpha + \beta x + \epsilon \]
Geometric view: the regression line passes through the data; \(\alpha\) is its intercept, \(\beta\) is its slope, and \(\epsilon_i\) is the vertical distance from each point to the line.
CAPM is a simple linear regression of a stock’s excess return on the market:
\[ Y = \alpha + \beta\, X + \epsilon \]
where \(Y = R_{\text{NVDA}} - R_f\) (NVDA excess return) and \(X = R_m - R_f\) (market excess return).
import yfinance as yf
import pandas_datareader.data as web
tickers = ["NVDA", "SPY"]
px = yf.download(tickers, start="2023-01-01", end="2024-12-31",
auto_adjust=True, progress=False)["Close"]
ret = px.pct_change().dropna()
ff = web.DataReader("F-F_Research_Data_5_Factors_2x3_daily",
"famafrench", start="2023-01-01", end="2024-12-31")
rf = ff[0]["RF"] / 100 # percent -> decimal
data = ret.join(rf, how="inner")
data["NVDA_excess"] = data["NVDA"] - data["RF"]
data["Mkt_excess"] = data["SPY"] - data["RF"]Why it matters
We subtract \(R_f\) because CAPM measures the premium over T-bills.
statsmodelsKey takeaway
Three steps: (1) sm.add_constant(X) adds \(\alpha\), (2) sm.OLS(y, X) defines the model, (3) .fit() estimates.
Why it matters
.fit() uses Ordinary Least Squares (OLS) — the same method from ISOM 2500. It finds \(\hat{\alpha}, \hat{\beta}\) that minimise \(\sum (y_i - \hat{y}_i)^2\).
| coef | std err | t | P>|t| | |
|---|---|---|---|---|
const (\(\alpha\)) |
0.0036 | 0.001 | 2.71 | 0.007 |
Mkt_excess (\(\beta\)) |
2.332 | 0.166 | 14.04 | 0.000 |
| \(R^2\) | 0.331 |
Why it matters
\(R^2 = 0.331\): market explains 33.1% of NVDA variation. Remaining 66.9% = idiosyncratic risk.
In any linear regression, the total variation in \(Y\) decomposes as:
\[ \underbrace{SST}_{\sum(y_i - \bar{y})^2} \;=\; \underbrace{SSR}_{\sum(\hat{y}_i - \bar{y})^2} \;+\; \underbrace{SSE}_{\sum(y_i - \hat{y}_i)^2} \qquad R^2 = \frac{SSR}{SST} \]
Why it matters
In CAPM: SSR = systematic risk (market), SSE = idiosyncratic risk (NVDA-specific).
statsmodels summaryFor \(t\)-tests and confidence intervals to be valid, the regression must satisfy:
Important
If LINE holds: \(\hat{\alpha}\) and \(\hat{\beta}\) follow \(t\)-distributions, \(p\)-values are trustworthy, CIs have correct coverage. If violated → \(p\)-values and CIs may be misleading.
The OLS estimates are random variables — different samples give different estimates.
Let \(s^2 = \dfrac{SSE}{n-2}\) (residual variance) and \(S_{xx} = \sum_{i}(x_i - \bar{x})^2\).
Standard errors:
\[ SE(\hat{\beta}) = \frac{s}{\sqrt{S_{xx}}} \qquad SE(\hat{\alpha}) = s\,\sqrt{\dfrac{1}{n} + \dfrac{\bar{x}^2}{S_{xx}}} \]
Why it matters
\(SE(\hat{\beta})\) shrinks when \(S_{xx}\) is large (wide spread in \(x\)), \(s\) is small (tight residuals), or \(n\) is large. More data spread \(\Rightarrow\) sharper slope estimate.
Coefficients tell you what the relationship is. Standard errors tell you how much to trust it.
1. They power every \(t\)-test:
\[ t_{\hat{\alpha}} = \frac{\hat{\alpha}}{SE(\hat{\alpha})} \qquad t_{\hat{\beta}} = \frac{\hat{\beta}}{SE(\hat{\beta})} \]
2. They determine CI width:
\[ \hat{\alpha} \pm t_{0.025}\!\cdot\! SE(\hat{\alpha}) \quad \hat{\beta} \pm t_{0.025}\!\cdot\! SE(\hat{\beta}) \]
Key takeaway
Coefficient = point estimate. SE = margin of error.
model.summary() Output| coef | std err | t | P>|t| | [0.025, 0.975] | |
|---|---|---|---|---|---|
| const (\(\alpha\)) | 0.0036 | 0.001 | 2.706 | 0.007 | 0.001, 0.006 |
| Mkt_excess (\(\beta\)) | 2.3320 | 0.166 | 14.038 | 0.000 | 2.005, 2.659 |
Key takeaway
Each row tests whether the coefficient is significantly different from zero. The P>|t| column gives the two-tailed \(p\)-value.
Under LINE, each estimate follows a \(t\)-distribution:
Slope (\(\beta\)):
\[ t_{\hat{\beta}} = \frac{\hat{\beta}}{SE(\hat{\beta})} \;\sim\; t_{n-2} \qquad H_0: \beta = 0 \]
\(p\)-value \(< 0.05\) \(\Rightarrow\) \(x\) is a significant predictor.
Intercept (\(\alpha\)):
\[ t_{\hat{\alpha}} = \frac{\hat{\alpha}}{SE(\hat{\alpha})} \;\sim\; t_{n-2} \qquad H_0: \alpha = 0 \]
Important
In CAPM, the slope test (\(H_0:\beta=0\)) answers: “Does the market explain NVDA at all?” The intercept test (\(H_0:\alpha=0\)) answers: “Does NVDA earn a free lunch above market risk?”
Why it matters
The \(p\)-value is the probability of seeing a result as extreme or more, assuming \(H_0\) is true.
Decision rule:
Example: \(p = 0.003\) for \(\hat{\beta}\) \(\Rightarrow\) only 0.3% chance of this slope if \(\beta = 0\) \(\Rightarrow\) \(X\) matters.
Key takeaway
Small \(p\) \(\Rightarrow\) unlikely under \(H_0\) \(\Rightarrow\) reject. \(p\) is not \(P(H_0 \text{ is true})\).
Sometimes we test a direction, not just “different from zero”.
Right-tailed: \(H_0: \beta \le 0\) vs \(H_a: \beta > 0\)
Left-tailed: \(H_0: \beta \ge 0\) vs \(H_a: \beta < 0\)
Key takeaway
statsmodels reports two-tailed \(p\). For one-tailed: divide by 2 only if the sign of \(t\) matches \(H_a\).
Does NVDA earn positive alpha?
\(H_0: \alpha \le 0\) vs \(H_a: \alpha > 0\)
Is NVDA more aggressive than the market?
\(H_0: \beta \le 1\) vs \(H_a: \beta > 1\)
Key takeaway
Default test: \(H_0: \beta = 0\) (does \(x\) matter?). To test a threshold like \(\beta = 1\), shift the null and recompute \(t = (\hat{\beta} - 1)/SE(\hat{\beta})\).
Instead of comparing \(p\) to \(0.05\), compare \(|t|\) to the critical value \(t_{\text{crit}}\).
Two-tailed (\(H_0: \beta = 0\)): Reject if \(|t| > t_{0.025,\,n-2}\)
One-tailed right (\(H_0: \beta \le 0\)): Reject if \(t > t_{0.05,\,n-2}\)
Key takeaway
Two equivalent approaches: (1) \(p\)-value \(< 0.05\), or (2) \(|t| > t_{\text{crit}}\). Both give the same conclusion. For large \(n\), \(t_{\text{crit}} \approx 1.96\) (two-tailed) or \(1.645\) (one-tailed).
A 95% confidence interval captures the true parameter with 95% probability:
\[ \text{CI for } \alpha:\quad \hat{\alpha} \;\pm\; t_{0.025,\,n-2} \cdot SE(\hat{\alpha}) \]
\[ \text{CI for } \beta:\quad \hat{\beta} \;\pm\; t_{0.025,\,n-2} \cdot SE(\hat{\beta}) \]
Key takeaway
CI for \(\beta\): \([1.66, 2.58]\) — we are 95% confident NVDA’s market sensitivity is between 1.66 and 2.58. CI for \(\alpha\): \([0.0008, 0.0085]\) — does not contain 0 \(\Rightarrow\) significant alpha.
model.summary() already reports everything:
| coef | std err | t | P>|t| | [0.025 | 0.975] | |
|---|---|---|---|---|---|---|
| const (\(\alpha\)) | 0.0036 | 0.001 | 2.71 | 0.007 | 0.0008 | 0.0085 |
| Mkt_excess (\(\beta\)) | 2.332 | 0.166 | 14.04 | 0.000 | 1.662 | 2.577 |
Why it matters
CI contains \(0\) \(\Leftrightarrow\) \(p\)-value \(> 0.05\). They carry the same information — always check both.
Try it!
Using the CAPM model on NVDA data:
statsmodels .get_prediction()Given a new predictor value \(x^*\) (e.g., market return \(= +1\%\)), we ask two different questions:
CI for \(\mu_Y \mid x^*\)
Prediction Interval for \(Y\)
Both intervals are centered at \(\hat{y}^* = \hat{\alpha} + \hat{\beta} x^*\).
CI for \(\mu_Y\):
\[ \hat{y}^* \pm t_{\alpha/2,\,n-2} \cdot s\sqrt{\dfrac{1}{n} + \dfrac{(x^*-\bar{x})^2}{S_{xx}}} \]
Prediction Interval:
\[ \hat{y}^* \pm t_{\alpha/2,\,n-2} \cdot s\sqrt{\color{red}{1} + \dfrac{1}{n} + \dfrac{(x^*-\bar{x})^2}{S_{xx}}} \]
Important
The “1” in the PI formula accounts for the individual error \(\epsilon\) in the new observation.
A single observation varies around the mean — even if we knew \(\mu_Y\) exactly, the PI could not shrink to zero.
PI is always wider than CI.
The CI band hugs the regression line tightly; the PI band is much wider because it has to cover individual scatter around the line.
Key takeaway
As \(n \to \infty\), the CI for \(\mu_Y\) narrows to a line (we learn \(\mu_Y\) exactly). The PI never collapses — individual variation \(\sigma^2\) is irreducible.
get_prediction() for Both IntervalsKey takeaway
mean_ci: CI for \(\mu_Y\) — NVDA’s expected average on +1% market days: \([+1.7\%, +2.1\%]\) obs_ci: PI for individual \(Y\) — tomorrow’s NVDA return: \([-1.1\%, +4.9\%]\)
The PI is 10× wider — individual returns are noisy even when the mean is precise.
Use CI for \(\mu_Y\) when…
Use PI when…
Try it!
If the market drops 2% tomorrow, use get_prediction() to find: (a) the 95% CI for NVDA’s expected return, and (b) the 95% PI for NVDA’s actual return. Which interval would a risk manager care about?
Why it matters
Train to learn, test to verify. If you test on training data, you are grading your own homework!
Diagram: full dataset → split into 80% Train (estimate \(\alpha, \beta\)) and 20% Test (predict).
Key rule: Time series — train on earlier dates, test on later dates. Never shuffle!
Two key metrics:
R\(^2\) (R-squared):
\[ R^2 = 1 - \frac{\sum (y_i - \hat{y}_i)^2}{\sum (y_i - \bar{y})^2} \]
RMSE:
\[ \text{RMSE} = \sqrt{\frac{1}{n-2}\sum (y_i - \hat{y}_i)^2} \]
Key takeaway
R\(^2\) = proportion explained. RMSE = typical prediction error. Always report both.
Key takeaway
If train and test R\(^2\) are close, the model generalises well — the CAPM beta estimated on past data is stable.
| Underfitting | Overfitting | |
|---|---|---|
| Cause | Model too simple | Model too complex |
| Train R\(^2\) | Low | High |
| Test R\(^2\) | Also low | Much lower than Train |
| Fix | Add predictors / nonlinear terms | Remove predictors / regularise |
Three regimes:
As model complexity grows, training error keeps falling but test error follows a U-shape — falling, then rising. The “sweet spot” is the minimum test error.
Important
Warning signs of overfitting:
Key takeaway
The goal is the sweet spot: complex enough to capture real patterns, simple enough to generalise. Adjusted R\(^2\) and cross-validation help find it.
Regression is trustworthy when residuals (\(e_i = y_i - \hat{y}_i\)) satisfy:
Important
For stock returns: I often violated (volatility clustering). N: daily returns have fat tails.
| Good | Bad |
|---|---|
| Random scatter around 0 | Curved (U-shape) pattern |
Key takeaway
How to read: Plot residuals (\(e\)) against fitted values (\(\hat{y}\)). Want a random cloud around 0. A curve means the linear model misses a nonlinear pattern — consider adding polynomial or transformed terms.
| Good | Bad |
|---|---|
| Random fluctuations | Volatility clustering (bunches of large residuals) |
Key takeaway
How to read: Plot residuals against time. Want no patterns or clusters. Bunches of large residuals = volatility clustering (very common in daily stock returns). Runs of same sign = autocorrelation. Both violate independence \(\Rightarrow\) standard errors are unreliable.
| Good | Bad |
|---|---|
| Points on the diagonal line | S-shape = fat tails |
Key takeaway
How to read: In a Q-Q plot, each point compares a sample quantile to the corresponding normal quantile. Points on the diagonal = normal. Deviations at the tails = heavy/fat tails (common in daily stock returns). Matters most for small samples; large \(n\) benefits from the CLT.
| Good (Homoscedastic) | Bad (Heteroscedastic) |
|---|---|
| Constant band width | Fan shape — \(\sigma\) grows |
Key takeaway
How to read: Residuals should have the same spread across all fitted values. A funnel/fan shape means variance depends on \(x\) (heteroscedasticity) \(\Rightarrow\) OLS standard errors are biased \(\Rightarrow\) \(p\)-values and CIs are unreliable. Fix: use robust (HC) standard errors or transform \(y\).
Four-panel diagnostic plot: residuals vs fitted (top-left), residuals over time (top-right), Q-Q plot (bottom-left), scale-location (bottom-right).
| Violated | Effect on Inference | Consequence |
|---|---|---|
| L Linearity | \(\hat{\beta}\) is biased; model misses pattern | Predictions systematically wrong; tests test the wrong model |
| I Independence | SEs are wrong (usually too small) | \(p\)-values too optimistic; CIs too narrow |
| N Normality | \(t\)/\(F\)-tests are approximate | Unreliable in small samples; OK for large \(n\) (CLT) |
| E Equal var. | OLS inefficient; SEs biased | Wrong CI width; \(p\)-values unreliable |
Important
L and I are the most damaging: they make the estimates themselves wrong or the uncertainty estimates wrong. Always check these first.
Key takeaway
N and E are less severe for large \(n\) (CLT helps). Always check L and I first — they are the hardest to fix.
| Assumptions Hold | Assumptions Fail |
|---|---|
| \(\hat{\beta}\) is unbiased | L fails \(\Rightarrow\) \(\hat{\beta}\) biased |
| SEs are correct | I or E fails \(\Rightarrow\) SEs wrong |
| \(t\)-tests & \(F\)-tests are exact | N fails \(\Rightarrow\) tests approximate |
| CIs have correct coverage | CIs may under/over-cover |
| Predictions are optimal | OLS no longer best estimator |
Key takeaway
OLS always gives numbers — but those numbers are only trustworthy if the assumptions approximately hold. Diagnostic plots let you check before trusting the output.
CAPM uses one factor (market return). But NVDA’s return may also depend on:
CAPM \(R^2 \approx 33\%\) — the market alone leaves 67% unexplained. Can we do better?
Important
Omitted variable bias: if a missing variable correlates with both \(X\) and \(Y\), \(\hat{\beta}\) is biased.
Key takeaway
MLR controls for multiple factors, giving each \(\beta_j\) a “holding all else constant” interpretation. CAPM → FF5.
Simple regression uses one predictor. In practice, outcomes depend on many variables:
\[ y = \alpha + \beta_1 x_1 + \beta_2 x_2 + \cdots + \beta_k x_k + \epsilon \]
Conceptual diagram: \(x_1, x_2, \ldots, x_k\) each carry a weight \(\beta_j\) into the prediction \(\hat{y}\).
The key difference from SLR: each \(\beta_j\) is a partial effect:
Why it matters
Example: Suppose we predict house price:
\(\hat{y} = 50 + 0.1 \times \text{sqft} + 15 \times \text{bedrooms}\)
\(\beta_{\text{sqft}} = 0.1\): each extra sq ft adds $100, holding bedrooms constant.
\(\beta_{\text{bed}} = 15\): each extra bedroom adds $15k, holding sqft constant.
Important
Without “holding constant”, \(\beta_j\) in MLR is not the same as \(\beta\) in SLR.
Adding correlated predictors changes all coefficients — this is not a bug, it is controlling for confounders.
# General MLR: identical syntax -- just pass multiple columns
# y = b0 + b1*x1 + b2*x2 + ... + bk*xk
X = sm.add_constant(df[["x1", "x2", "x3"]]) # k predictors
model = sm.OLS(y, X).fit()
print(model.summary())
# Key outputs:
# model.params -> coefficients (beta_0, ..., beta_k)
# model.pvalues -> p-value for each beta_j
# model.rsquared_adj -> Adjusted R-squared
# model.conf_int() -> 95% CIs for all betasKey takeaway
The code is identical to SLR — just pass a DataFrame with \(k\) columns instead of one. OLS handles everything.
Business question: A pharmacy chain has 111 branches across US metro areas. What local demographics predict per-branch profit?
Predictors (6 features): Income, Disposable Income, Birth Rate, Soc Security, CV Death, % 65 or Older.
Target: Profit per branch.
Why this dataset?
Real business case, small enough to read by eye, large enough to need multiple regression. Profit depends on multiple local features — no single one tells the full story.
After running, look at:
coef — direction & magnitudeP>|t| — which features are statistically significantReading the coefficients
Try it!
Drop the insignificant features and refit. Does Adjusted R² go up or down? What does that tell you about model parsimony?
Suppose a new branch opens in a city with these demographics. What does the model predict?
CI vs PI for the business
CI for mean — “If we opened many branches with these demographics, average profit would be in this range.” Used for portfolio decisions.
PI for individual — “This specific branch’s profit will be in this range.” Wider — more cautious for single-branch decisions.
Problem: R\(^2\) always increases when you add variables — even useless ones!
R\(^2\):
\[ R^2 = 1 - \frac{SSE}{SST} \]
Adding any variable can only decrease SSE, so R\(^2\) never drops.
Adjusted R\(^2\):
\[ \bar{R}^2 = 1 - \frac{(1-R^2)(n-1)}{n-k-1} \]
Penalises for \(k\). Can decrease if a new variable does not help enough.
Key takeaway
Use \(\bar{R}^2\) to compare models with different numbers of predictors.
Problem: When predictors are correlated with each other, OLS still works but coefficients become unstable.
Symptoms:
Variance Inflation Factor:
\[ \text{VIF}_j = \frac{1}{1 - R_j^2} \]
where \(R_j^2\) = R\(^2\) from regressing \(x_j\) on all other \(x\)’s.
| VIF | Interpretation |
|---|---|
| \(< 5\) | OK |
| \(5\)–\(10\) | Moderate |
| \(> 10\) | Severe |
Important
Multicollinearity does not bias predictions — it only inflates uncertainty around individual \(\beta_j\)’s.
from statsmodels.stats.outliers_influence import (
variance_inflation_factor)
# Compute VIF for each predictor
X_vif = sm.add_constant(df[["x1", "x2", "x3"]])
for i, col in enumerate(["x1", "x2", "x3"]):
vif = variance_inflation_factor(X_vif.values, i + 1)
print(f"{col}: VIF = {vif:.2f}")Key takeaway
Rule of thumb: If any VIF \(> 10\), consider dropping or combining the offending variable. VIF \(< 5\) is generally safe.
Setup: 3 predictors — \(x_1\) (age), \(x_2\) (income), \(x_3\) (credit score). To get VIF for \(x_2\): regress \(x_2\) on \(x_1, x_3\), get \(R_2^2\), then \(\text{VIF}_2 = 1/(1 - R_2^2)\).
| Predictor | \(R_j^2\) | VIF | Status |
|---|---|---|---|
| \(x_1\) (age) | 0.10 | 1.11 | OK |
| \(x_2\) (income) | 0.85 | 6.67 | Warn |
| \(x_3\) (credit) | 0.82 | 5.56 | Warn |
Why it matters
\(R_j^2 = 0.85\): 85% of income explained by others \(\Rightarrow\) redundant. \(R_j^2 = 0.10\): age is independent of others.
Key takeaway
High \(R_j^2\) \(\Rightarrow\) \(x_j\) well-predicted by others \(\Rightarrow\) high VIF \(\Rightarrow\) inflated \(SE(\hat{\beta}_j)\). Fix: drop or combine correlated predictors.
CAPM: one factor explains $$33%. What else matters?
Fama-French 5-Factor Model:
\[ R - R_f = \alpha + \beta_1 \text{Mkt} + \beta_2 \text{SMB} + \beta_3 \text{HML} + \beta_4 \text{RMW} + \beta_5 \text{CMA} + \epsilon \]
Why it matters
Each \(\beta_j\) captures a different type of systematic risk. Together they explain more than the market alone.
| Factor | Long / Short | Intuition |
|---|---|---|
| Mkt-RF | Broad market | Core market risk |
| SMB | Small / large caps | Small firms tend to outperform |
| HML | High B/M / low B/M | Value beats growth |
| RMW | High / low profit | Profitable firms earn more |
| CMA | Low / high investment | Conservative firms outperform |
Key takeaway
For tech growth (NVDA): expect large \(\beta_1\), negative \(\beta_2\) (large cap), negative \(\beta_3\) (growth \(\ne\) value).
import pandas_datareader.data as web
ff = web.DataReader("F-F_Research_Data_5_Factors_2x3_daily",
"famafrench", start="2023-01-01", end="2024-12-31")
ff_factors = ff[0] / 100 # percent -> decimal
data_ff = ret[["NVDA"]].join(ff_factors, how="inner")
data_ff["NVDA_excess"] = data_ff["NVDA"] - data_ff["RF"]
print(data_ff.head(3))Why it matters
pandas_datareader pulls the official Fama-French factors from Ken French’s data library.
Key takeaway
The code is identical to simple regression — just pass a list of five factor columns. statsmodels handles everything.
| Factor | \(\beta\) | p-value | Meaning |
|---|---|---|---|
| const (\(\alpha\)) | 0.0006 | 0.56 | No significant alpha |
| Mkt-RF | 1.84 | 0.000 | High market sensitivity |
| SMB | \(-0.32\) | 0.03 | Behaves like large-cap |
| HML | \(-0.65\) | 0.000 | Strong growth tilt |
| RMW | \(-0.48\) | 0.01 | Lower vs. high-profit |
| CMA | \(-0.22\) | 0.18 | Not significant |
Key takeaway
Each \(\beta_j\): “holding other factors constant, a 1-unit increase in this factor changes NVDA excess return by \(\beta_j\).”
Individual \(t\)-test (same as SLR):
\[ H_0: \beta_j = 0 \quad\text{vs}\quad H_a: \beta_j \ne 0 \qquad t_j = \frac{\hat{\beta}_j}{SE(\hat{\beta}_j)} \;\sim\; t_{n-k-1} \]
Overall \(F\)-test (are any factors useful?):
\[ H_0: \beta_1 = \beta_2 = \cdots = \beta_k = 0 \qquad F = \frac{SSR/k}{SSE/(n-k-1)} \;\sim\; F_{k,\,n-k-1} \]
Key takeaway
\(F\)-test significant \(\Rightarrow\) at least one factor matters. Individual \(t\)-tests tell you which ones. CMA (\(p = 0.18\)): candidate for dropping.
Why it matters
FF5 factors are constructed to be approximately orthogonal, so VIFs are typically low. But HML and CMA can be correlated (both relate to firm investment).
Key takeaway
If VIF \(> 10\): drop or combine. Here all VIFs are low \(\Rightarrow\) multicollinearity is not a problem for NVDA’s factor model.
Key takeaway
Compare using Adjusted R\(^2\) (not R\(^2\), which always increases). If FF5 \(\bar{R}^2 >\) CAPM \(\bar{R}^2\), the extra factors genuinely add explanatory power beyond the market alone.
The SLR formulas extend naturally to MLR. For a new observation \(\mathbf{x}^* = (x_1^*, \ldots, x_k^*)\):
CI for \(\mu_Y \mid \mathbf{x}^*\)
Average NVDA return on all days with these factor values. Width depends on \(SE(\hat{\mu})\) only — shrinks with more data.
Prediction Interval for \(Y\)
Tomorrow’s NVDA return given these factor values. Adds \(\sigma^2\) for individual noise — always wider than CI.
Test whether any predictor is useful:
\[ H_0: \beta_1 = \beta_2 = \cdots = \beta_k = 0 \quad\text{vs}\quad H_a: \text{at least one } \beta_j \neq 0 \]
\[ F = \frac{SSR/k}{SSE/(n-k-1)} = \frac{R^2/k}{(1-R^2)/(n-k-1)} \;\sim\; F_{k,\,n-k-1} \]
Key takeaway
The F-test is in every model.summary(). An insignificant F (p \(>\) 0.05) means none of your variables explains the response — the model is useless.
Goal: Find the single best model of each size \(k = 1, 2, \ldots, p\).
Algorithm:
Visual: plot every candidate model’s Adj \(R^2\) vs \(k\); an upper-envelope “frontier” gives the best at each size, and the optimal complexity is the peak of that frontier.
isom2600.regressionimport isom2600
# Prepare data: X must be a DataFrame, Y a Series
X = train_ff[factors] # 5 Fama-French factors
Y = train_ff["NVDA_excess"]
# Stage 1: best model of each size (by SSE)
df = isom2600.regression.best_subset_init(X, Y)
df_s1 = isom2600.regression.best_subset_stage1(df)
# Stage 2: pick best overall by each criterion
best_adjR2 = isom2600.regression.best_subset_stage2(df_s1, "Adj_R2")
best_AIC = isom2600.regression.best_subset_stage2(df_s1, "AIC")
best_BIC = isom2600.regression.best_subset_stage2(df_s1, "BIC")
print("Adj R2 selects:", best_adjR2)
print("AIC selects:", best_AIC)
print("BIC selects:", best_BIC)Why it matters
BIC penalises complexity more than AIC — it tends to pick a sparser model. They may agree (good) or disagree (check both).
Recall: \(SSE = \sum_{i=1}^{n}(y_i - \hat{y}_i)^2\) (sum of squared residuals), \(k\) = number of predictors.
| Criterion | Formula | Penalises | Select |
|---|---|---|---|
| Adj R\(^2\) | \(1 - \tfrac{(1-R^2)(n-1)}{n-k-1}\) | Weak | Maximise |
| AIC | \(n\ln(\tfrac{SSE}{n}) + 2k\) | Moderate | Minimise |
| BIC | \(n\ln(\tfrac{SSE}{n}) + k\ln n\) | Strong | Minimise |
| \(C_p\) | \(\tfrac{SSE}{\hat\sigma^2} - n + 2k\) | Moderate | \(C_p \approx k\) |
Key takeaway
AIC optimises prediction accuracy. BIC optimises model identification (consistent). For small samples (\(n < 40\)) use BIC. For forecasting use AIC (or cross-validation).
What does “Penalises” mean? Each criterion has a fit term (always improves with more \(x\)’s) and a penalty term that punishes complexity. “Penalises” rates how strongly that penalty pushes back.
Why “Select: \(C_p \approx k\)”?
Theorem: if the model is correctly specified, \(E[C_p] \approx k\). So we look for \(C_p\) close to its own \(k\):
What does “consistent” mean for BIC?
As \(n \to \infty\), BIC picks the true model with probability \(\to 1\). AIC does not.
Why? BIC’s penalty \(k\ln n\) grows with \(n\), eventually crushing any spurious variable. AIC’s \(2k\) stays fixed.
Trade-off: AIC is efficient (best predictions); BIC is consistent (right model). No criterion is both.
Key takeaway
For forecasting NVDA returns → AIC. For which factors really drive returns (inference) → BIC.
| Model | \(k\) | Train Adj R\(^2\) | Test R\(^2\) | BIC rank |
|---|---|---|---|---|
| CAPM | 1 | 0.543 | 0.521 | 3 |
| FF4 | 4 | 0.582 | 0.563 | 1 |
| FF5 | 5 | 0.581 | 0.558 | 2 |
Key takeaway
Best model = FF4. Extra factors add variance without reducing bias. Occam’s razor confirmed by data.
Scenario: Market up 1.5% — what do we expect from NVDA?
Why it matters
With \(\beta \approx 2.33\), on a \(+1.5\%\) market day NVDA is predicted to gain \(\approx +3.5\%\).
Question: Does NVDA consistently outperform the Fama-French model?
Key takeaway
A significant \(\alpha\) means NVDA has genuine excess returns beyond its factor exposures. In an efficient market, this is rare.
Try it!
Pick any stock (AAPL, TSLA, MSFT, AMD) and:
yfinancepandas_datareaderHint: Start with tickers = ["AAPL", "SPY"] and reuse the pipeline.
Pipeline: Explore → Visualize → Build → Evaluate → Select → Apply
| Step | Tool |
|---|---|
| Explore | .corr() |
| Visualize | regplot |
| Build | sm.OLS |
| Evaluate | R\(^2\), RMSE |
| Select | p-values, AIC/BIC |
| Apply | .predict() |
Key takeaway
SLR = CAPM: one market factor, \(\beta\) measures risk exposure.
MLR = Fama-French: five factors explain more variation in returns.
Variable selection: only keep factors that are statistically significant.
Next: Topic 4 — Clustering (K-Means & Hierarchical).
Prof. Xuhu Wan · HKUST ISOM · Intro to Business Analytics