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-17 -0.009957 -0.003679 -0.000746 0.005138 0.001017 0.00006
2026-07-20 -0.001674 0.007068 0.005252 -0.000846 -0.001437 0.00006
2026-07-21 0.008281 -0.005705 0.006582 -0.000939 -0.002167 0.00006
2026-07-22 -0.001223 0.001658 -0.001945 -0.002092 0.001974 0.00006
2026-07-23 -0.012409 -0.004686 0.003941 -0.002240 -0.002404 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.16 0.002 0.000 0.001
Mkt-RF 1.130 0.020 56.55 0.000 1.091 1.170
SMB 0.030 0.061 0.50 0.619 -0.089 0.150
HML 0.042 0.061 0.69 0.493 -0.078 0.161
RMW 0.156 0.082 1.89 0.058 -0.005 0.317
CMA 0.006 0.081 0.07 0.941 -0.153 0.165
Alpha                     0.000767
Alpha (ann.)              0.193368
R²                        0.324308
Adj. R²                   0.323802
Residual Volatility       0.314958
Periods                6677.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-17 0.004070 0.528967 0.523469 0.257803 -0.888892 0.134841 0.091716 63.0
2026-07-20 0.003146 0.505502 0.271942 0.039186 -0.671713 0.130757 0.066079 63.0
2026-07-21 0.002953 0.504827 0.262997 0.063215 -0.676527 0.102816 0.066807 63.0
2026-07-22 0.003210 0.446462 0.126371 0.052884 -0.649881 0.221091 0.056936 63.0
2026-07-23 0.002819 0.425729 0.107335 -0.087631 -0.586815 0.477752 0.058646 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-17 0.000767 -0.011256 -0.000112 -0.000031 0.000800 0.000006 0.011205
2026-07-20 0.000767 -0.001893 0.000215 0.000219 -0.000132 -0.000009 -0.020652
2026-07-21 0.000767 0.009361 -0.000174 0.000274 -0.000146 -0.000013 -0.006609
2026-07-22 0.000767 -0.001382 0.000050 -0.000081 -0.000326 0.000012 -0.004745
2026-07-23 0.000767 -0.014028 -0.000143 0.000164 -0.000349 -0.000015 0.000562
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.100051
CAGR               0.082151
Volatility         0.192924
Sharpe             0.505775
Sortino            0.716277
Calmar             0.148852
Max Drawdown      -0.551894
Win Rate           0.546424
Periods         6677.000000
Name: SPY, dtype: float64
Back to top