Factor analytics

Multi-factor regression analytics (e.g. the Fama-French five-factor model) decompose a return stream’s excess return into exposures (loadings, or betas) to a set of systematic factors, plus an unexplained alpha. q.stats fits these regressions (full-period and rolling) and attributes return to each factor; q.plot renders the loadings, rolling betas, and cumulative contributions. These code cells are executed live by Quarto every time the docs are built.

Factor data

q.stats.factor_regression and friends take any factors DataFrame of periodic factor return columns aligned to a strategy’s returns — not only the Fama-French five. qrt doesn’t bundle the Kenneth French Data Library’s published factors, so this page builds a stand-in five-factor set from qrt’s bundled SPY dataset (Mkt-RF = SPY’s own excess return) plus small synthetic SMB/HML/RMW/CMA series, purely to demonstrate the API offline. Swap in a real download from Ken French’s site (or another factor provider) and everything below works unchanged — just make sure factor and asset returns are both decimal (Ken French’s own files are in percent, e.g. 1.25 meaning 0.0125).

import numpy as np
import pandas as pd

import qrt as q

aapl = q.data.datasets.load("aapl")
spy = q.data.datasets.load("spy")

strategy = aapl["close"].pct_change().rename("AAPL")
market = spy["close"].pct_change().rename("Market")
aligned = pd.concat([strategy, market], axis=1).dropna()
strategy, market = aligned["AAPL"], aligned["Market"]

rng = np.random.default_rng(7)
rf = pd.Series(0.00006, index=aligned.index, name="RF")
factors = pd.DataFrame(
    {
        "Mkt-RF": market - rf,
        "SMB": rng.normal(0.0, 0.004, len(aligned)),
        "HML": rng.normal(0.0, 0.004, len(aligned)),
        "RMW": rng.normal(0.0, 0.003, len(aligned)),
        "CMA": rng.normal(0.0, 0.003, len(aligned)),
        "RF": rf,
    },
    index=aligned.index,
)
factors.tail()
Mkt-RF SMB HML RMW CMA RF
datetime
2026-07-13 -0.007716 -0.000417 -0.002230 0.001814 -0.002818 0.00006
2026-07-14 0.003491 -0.005345 -0.003685 -0.003583 -0.000894 0.00006
2026-07-15 0.003904 -0.004932 -0.002018 0.001700 -0.000454 0.00006
2026-07-16 -0.005479 0.003687 -0.008660 -0.001954 0.001310 0.00006
2026-07-17 -0.009957 -0.003679 -0.001596 0.003710 -0.000667 0.00006

Full-period factor loadings

q.stats.factor_regression fits one OLS regression over every aligned observation, returning a coefficient table (with standard errors, t-statistics, p-values, and confidence intervals) indexed by Alpha and each factor column. q.stats.factor_regression_stats reports the fit’s overall diagnostics (R², annualized alpha, residual volatility):

loadings = q.stats.factor_regression(strategy, factors)
display(loadings.style.format({
    "Coefficient": "{:.3f}", "Std Error": "{:.3f}", "t-Statistic": "{:.2f}",
    "p-Value": "{:.3f}", "CI Lower": "{:.3f}", "CI Upper": "{:.3f}",
}))

q.stats.factor_regression_stats(strategy, factors)
  Coefficient Std Error t-Statistic p-Value CI Lower CI Upper
Alpha 0.001 0.000 3.18 0.002 0.000 0.001
Mkt-RF 1.129 0.020 56.48 0.000 1.090 1.169
SMB 0.034 0.061 0.56 0.576 -0.086 0.154
HML 0.013 0.061 0.22 0.827 -0.106 0.133
RMW -0.064 0.082 -0.78 0.433 -0.226 0.097
CMA 0.017 0.081 0.20 0.839 -0.143 0.176
Alpha                     0.000772
Alpha (ann.)              0.194534
R²                        0.323957
Adj. R²                   0.323450
Residual Volatility       0.315103
Periods                6673.000000
Name: AAPL, dtype: float64

q.plot.factor_loadings renders the same coefficients as a bar chart with 95% confidence-interval error bars and a zero reference line:

fig = q.plot.factor_loadings(strategy, factors, title="AAPL — Fama-French 5-Factor Loadings")
fig.show()

Rolling factor betas

A single full-period fit hides time-varying exposure. q.stats.rolling_factor_regression refits the same OLS on a trailing window (63 periods ≈ 3 trading months by default), storing each estimate at the date of its window’s last observation — the first window - 1 rows have no complete window yet and are NaN:

window = 63
rolling = q.stats.rolling_factor_regression(strategy, factors, window=window)
rolling.dropna().tail()
Alpha Mkt-RF SMB HML RMW CMA N Obs
datetime
2026-07-13 0.001803 0.620686 0.462592 0.188766 0.089763 -1.115087 0.116234 63.0
2026-07-14 0.001860 0.629325 0.532052 0.254529 0.137317 -1.036920 0.117082 63.0
2026-07-15 0.002391 0.676995 0.368524 0.087881 0.267389 -1.073607 0.113147 63.0
2026-07-16 0.002440 0.621933 0.382512 -0.208358 0.068409 -0.918684 0.093283 63.0
2026-07-17 0.002673 0.636596 0.407909 -0.386733 0.265439 -1.068613 0.105947 63.0
fig = q.plot.rolling_factor_betas(strategy, factors, window=window, title=f"AAPL — Rolling Factor Betas ({window}-day window)")
fig.show()

Factor contributions

A loading measures sensitivity; a contribution measures how much return that sensitivity actually generated. q.stats.factor_contributions attributes each period’s excess return to Alpha, one term per factor (its full-period coefficient times that period’s factor return), and a Residual. The columns sum exactly to the excess return every period, so their arithmetic (not compounded) cumulative sums reconstruct the cumulative excess return exactly — q.plot.factor_contributions overlays that total as a dotted reference line:

contributions = q.stats.factor_contributions(strategy, factors)
contributions.tail()
Alpha Mkt-RF SMB HML RMW CMA Residual
datetime
2026-07-13 0.000772 -0.008715 -0.000014 -0.000030 -0.000117 -0.000047 0.014402
2026-07-14 0.000772 0.003943 -0.000183 -0.000049 0.000231 -0.000015 -0.012480
2026-07-15 0.000772 0.004409 -0.000169 -0.000027 -0.000110 -0.000008 0.035217
2026-07-16 0.000772 -0.006188 0.000126 -0.000115 0.000126 0.000022 0.022785
2026-07-17 0.000772 -0.011246 -0.000126 -0.000021 -0.000239 -0.000011 0.012252
fig = q.plot.factor_contributions(strategy, factors, title="AAPL — Cumulative Factor Contribution")
fig.show()

Benchmark statistics

Standalone benchmark performance (CAGR, volatility, Sharpe, Max Drawdown, …) is not an output of the factor regression – it’s the ordinary return-stream stats already covered on Performance & benchmark, computed directly from the benchmark’s own return stream via q.stats.performance:

q.stats.performance(market.rename("SPY"))
Total Return       7.156124
CAGR               0.082484
Volatility         0.192959
Sharpe             0.507314
Sortino            0.718487
Calmar             0.149456
Max Drawdown      -0.551894
Win Rate           0.546603
Periods         6673.000000
Name: SPY, dtype: float64
Back to top