factrix.metrics.ts_beta ¶
Time-series beta metrics for macro common factors.
macro_common factors (VIX, gold, USD index) are a single time series shared across all assets. Per-asset time-series regression measures each asset's sensitivity (β) to the common factor.
compute_ts_betas: per-asset full-sample TS regression.
ts_beta: cross-sectional test on the β distribution.
mean_r_squared: average explanatory power across assets.
compute_rolling_mean_beta: rolling window mean β for stability analysis.
Notes
Pipeline. Per-asset full-sample ordinary least squares (OLS) β (time-series step), then cross-asset t on the β distribution; rolling-window variant slices the time axis before the per-asset step.
factrix.metrics.ts_beta.compute_ts_betas ¶
compute_ts_betas(df: DataFrame, *, factor_col: str = 'factor', return_col: str = 'forward_return') -> DataFrame
Per-asset time-series ordinary least squares (OLS): R_{i,t} = α_i + β_i · F_t + ε.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
df
|
DataFrame
|
Long panel with |
required |
factor_col
|
str
|
Column carrying the (broadcast) factor. |
'factor'
|
return_col
|
str
|
Column carrying the per-asset forward return. |
'forward_return'
|
Returns:
| Type | Description |
|---|---|
DataFrame
|
DataFrame with ``asset_id, beta, alpha, t_stat, r_squared, |
DataFrame
|
n_obs |
DataFrame
|
singular design are dropped. |
Notes
Per asset i, run OLS R_{i,t} = alpha_i + beta_i * F_t +
eps over the asset's full sample with homoskedastic SE; emit
beta_i, alpha_i, t_i, R^2_i, n_i. The output is the
per-asset stage feeding the cross-asset aggregation in
ts_beta (time-series-then-cross-section order in the
Black-Jensen-Scholes (BJS) tradition).
factrix reports homoskedastic per-asset t (not Newey-West (NW) heteroskedasticity-and-autocorrelation-consistent (HAC)) at this
stage because the inferential burden lives downstream — the
cross-asset t in ts_beta is what a caller decides on, and
adding HAC at the per-asset stage would only smear the
per-asset diagnostic without affecting stage-2 inference.
References
Black-Jensen-Scholes 1972:
beta-sorted-portfolio time-series CAPM tests; the two-stage
time-series-then-cross-section aggregation order is what
ts_beta adopts. The cross-asset t on mean β here is a
simplified analogue of that aggregation order, not a
replication of the original BJS grouped-portfolio intercept
test.
Examples:
>>> import factrix as fx
>>> from factrix.preprocess import compute_forward_return
>>> from factrix.metrics.ts_beta import compute_ts_betas
>>> panel = compute_forward_return(
... fx.datasets.make_cs_panel(n_assets=80, n_dates=180, seed=0),
... forward_periods=5,
... )
>>> ts_betas_df = compute_ts_betas(panel)
>>> set(ts_betas_df.columns) >= {"asset_id", "beta", "r_squared"}
True
factrix.metrics.ts_beta.ts_beta ¶
ts_beta(ts_betas_df: DataFrame) -> MetricOutput
Test \(H_0: \mathrm{mean}(\beta) = 0\) across assets.
Uses the cross-sectional distribution of per-asset betas.
Notes
Stage 2 of the BJS-style aggregation order:
\(\overline{\beta} = \mathrm{mean}_i \beta_i\);
\(t = \overline{\beta} / (\mathrm{std}(\beta) / \sqrt{N})\)
with \(H_0: \mathbb{E}[\beta] = 0\) across assets. The std is the
sample cross-sectional std with ddof=1.
factrix uses an iid cross-asset t at this stage rather than a
clustered/heteroskedasticity-and-autocorrelation-consistent (HAC) variant: per-asset betas come from non-overlapping
time-series fits in compute_ts_betas, so the betas are
approximately independent across assets unless a strong
latent common factor links them.
References
Black-Jensen-Scholes 1972: beta-sorted-portfolio time-series CAPM tests. factrix's cross-asset t on mean β is a simplified analogue of the BJS aggregation order, not a replication of the grouped-portfolio intercept test BJS run on assets sorted into beta deciles.
Examples:
Chain from :func:compute_ts_betas output:
>>> import factrix as fx
>>> from factrix.preprocess import compute_forward_return
>>> from factrix.metrics.ts_beta import compute_ts_betas, ts_beta
>>> panel = compute_forward_return(
... fx.datasets.make_cs_panel(n_assets=80, n_dates=180, seed=0),
... forward_periods=5,
... )
>>> ts_betas_df = compute_ts_betas(panel)
>>> result = ts_beta(ts_betas_df)
>>> result.name
'ts_beta'
factrix.metrics.ts_beta.mean_r_squared ¶
mean_r_squared(ts_betas_df: DataFrame) -> MetricOutput
Average \(R^2\) across per-asset TS regressions — value \(= \mathrm{mean}_i R^2_i\).
\(R^2_i\) comes from asset \(i\)'s regression
\(R_{i,t} = \alpha_i + \beta_i \cdot F_t + \varepsilon\) (computed
upstream in compute_ts_betas). Metadata carries
median_r_squared as well — useful when a few high-\(R^2\) assets
pull the mean. Low values (\(< 0.05\)) indicate the factor is too
weak or noisy to drive individual-asset returns even when its
cross-asset mean \(\beta\) looks nonzero.
Short-circuits to NaN when no assets have a non-null \(R^2\).
Notes
value \(= \mathrm{mean}_i R^2_i\) and median_r_squared
\(= \mathrm{median}_i R^2_i\) on the per-asset ordinary least squares (OLS) fits from
compute_ts_betas. Pure descriptive statistic — no formal
\(H_0\).
factrix reports both mean and median because a few high-\(R^2\) assets can dominate the mean; large mean-vs-median gaps signal the factor explains a small subset of assets rather than the cross-section as a whole.
Examples:
Chain from :func:compute_ts_betas output:
>>> import factrix as fx
>>> from factrix.preprocess import compute_forward_return
>>> from factrix.metrics.ts_beta import compute_ts_betas, mean_r_squared
>>> panel = compute_forward_return(
... fx.datasets.make_cs_panel(n_assets=80, n_dates=180, seed=0),
... forward_periods=5,
... )
>>> ts_betas_df = compute_ts_betas(panel)
>>> result = mean_r_squared(ts_betas_df)
>>> result.name
'mean_r_squared'
factrix.metrics.ts_beta.ts_beta_sign_consistency ¶
ts_beta_sign_consistency(ts_betas_df: DataFrame) -> MetricOutput
Symmetric sign-agreement across per-asset βs — value = max(pos, 1−pos) where pos = mean_i 1{β_i > 0}.
Range [0.5, 1.0]: 0.5 = βs evenly split (no directional consensus);
1.0 = all βs share one sign. Unlike
fama_macbeth.beta_sign_consistency this is direction-agnostic
— it does not require a prior on the factor's expected sign.
Requires N ≥ 2: a single β is trivially "100% consistent with itself" (the max collapses to 1.0 for any nonzero β), which would read as strong evidence on a dashboard but carries zero information. Short-circuits to NaN in that case so the degenerate value never leaks into downstream inference.
Notes
pos = mean_i 1{beta_i > 0}; value = max(pos, 1 - pos).
Direction-agnostic: returns 1 when all assets have positive
beta or all negative.
factrix gates this metric at N >= 2 so a single-asset
max(pos, 1-pos) = 1.0 cannot leak into downstream
inference as spurious "perfect agreement". Pair with
fama_macbeth.beta_sign_consistency when a directional prior
is available.
Examples:
Chain from :func:compute_ts_betas output:
>>> import factrix as fx
>>> from factrix.preprocess import compute_forward_return
>>> from factrix.metrics.ts_beta import (
... compute_ts_betas,
... ts_beta_sign_consistency,
... )
>>> panel = compute_forward_return(
... fx.datasets.make_cs_panel(n_assets=80, n_dates=180, seed=0),
... forward_periods=5,
... )
>>> ts_betas_df = compute_ts_betas(panel)
>>> result = ts_beta_sign_consistency(ts_betas_df)
>>> result.name
'ts_beta_sign_consistency'
factrix.metrics.ts_beta.compute_rolling_mean_beta ¶
compute_rolling_mean_beta(df: DataFrame, *, window: int = 60, factor_col: str = 'factor', return_col: str = 'forward_return') -> DataFrame
Rolling-window mean β across assets — time-series input for out-of-sample (OOS) / trend.
Formula (per date t ≥ window):
For each asset i, take the trailing window rows ending at t.
If ≥ 10 valid (factor, return) pairs, run ordinary least squares (OLS):
R_{i,s} = α_i + β_i·F_s + ε (s in window)
β_t = mean_i β_i (cross-asset mean of this window's βs)
Dates with fewer than window trailing rows are skipped. Assets
with < 10 valid obs in the window are dropped from that date's β
calculation. If no asset qualifies at a given date, that date is
absent from the output entirely.
Returns:
| Type | Description |
|---|---|
DataFrame
|
DataFrame with |
DataFrame
|
cross-asset mean β. Shape compatible with |
Notes
Per date t >= window, run the per-asset TS OLS over the
trailing window rows and compute value_t = mean_i beta_i.
Output schema matches the time-series tools (oos /
ic_trend), so callers can pipe rolling betas into stability
and trend diagnostics.
factrix requires at least 10 valid rows per asset within each
rolling window; below that, the asset is dropped from that
date's mean rather than imputed — keeps each value_t an
average over identifiable per-asset slopes.
Examples:
>>> import factrix as fx
>>> from factrix.preprocess import compute_forward_return
>>> from factrix.metrics.ts_beta import compute_rolling_mean_beta
>>> panel = compute_forward_return(
... fx.datasets.make_cs_panel(n_assets=80, n_dates=180, seed=0),
... forward_periods=5,
... )
>>> rolling = compute_rolling_mean_beta(panel, window=60)
>>> set(rolling.columns) >= {"date", "value"}
True
Timeseries-mode conventions
Stage-1 per-asset ordinary least squares (OLS) uses plain SE, not heteroskedasticity-and-autocorrelation-consistent (HAC) — the dominant
bias under a persistent predictor is Stambaugh coefficient bias,
which HAC does not address. FACTOR_ADF_P is emitted on the input
series; non-overlap resampling is not applied. See
Timeseries-mode conventions
for the full rationale.
Use cases¶
-
Compute per-asset TS betas
Stage 1 of the Black-Jensen-Scholes aggregation: per-asset OLS \(R_{i,t} = \alpha_i + \beta_i \cdot F_t + \varepsilon\) over each asset's full sample. Pre-step for
ts_beta/mean_r_squared/ts_beta_sign_consistency. Assets with fewer thanMIN_TS_OBSrows or a singular design are dropped. -
Cross-asset mean-\(\beta\) significance
Stage 2 of BJS: \(t = \overline{\beta} / (\mathrm{std}(\beta) / \sqrt{N})\) with \(H_0: \mathbb{E}[\beta] = 0\) across assets. Cross-asset iid \(t\) is used because the per-asset betas come from non-overlapping time-series fits and are approximately independent unless a strong latent common factor links them.
-
Explanatory power across the cross-section
mean_r_squaredreports \(\overline{R^2}\) andmedian_r_squaredon the per-asset fits. Low values (\(< 0.05\)) say the factor is too weak or noisy to drive individual-asset returns even when its cross-asset mean \(\beta\) looks nonzero; large mean-vs-median gaps say the factor explains a small subset of assets rather than the cross-section as a whole. -
Rolling-window stability
compute_rolling_mean_betaemits a(date, value)series of rolling cross-asset mean \(\beta\) at stridewindow. Output schema matches the time-series tools so callers can pipe rolling betas intotrend/oos.
Choosing a function¶
| Goal | Function |
|---|---|
| Per-asset TS beta table for downstream inspection / slicing | compute_ts_betas |
| Mean-\(\beta\) significance across assets (Stage 2 of BJS) | ts_beta |
| Average explanatory power \(\overline{R^2}\) across assets | mean_r_squared |
| Direction-agnostic sign agreement on per-asset \(\beta\) | ts_beta_sign_consistency |
| Rolling cross-asset mean \(\beta\) series for trend / out-of-sample (OOS) pipes | compute_rolling_mean_beta |
| \(N=1\) degenerate-case fallback used by Profile / Factor entry points | ts_beta_single_asset_fallback |
Worked example — per-asset TS betas then cross-asset \(t\)¶
compute_ts_betas → ts_beta on a broadcast common-factor panel
import factrix as fx
import polars as pl
from factrix.metrics.ts_beta import compute_ts_betas, ts_beta, mean_r_squared
from factrix.preprocess import compute_forward_return
# Build a panel where ``factor`` is broadcast (one value per date,
# shared across all assets) — VIX / USD-index / NFP surprise style.
raw = fx.datasets.make_cs_panel(
n_assets=50, n_dates=500, ic_target=0.08, seed=2024,
)
common = (
raw.group_by("date").agg(pl.col("factor").mean().alias("factor"))
)
panel = (
raw.drop("factor").join(common, on="date")
)
panel = compute_forward_return(panel, forward_periods=5)
betas_df = compute_ts_betas(panel)
print(betas_df.head())
# ┌──────────┬─────────┬─────────┬────────┬───────────┬───────┐
# │ asset_id ┆ beta ┆ alpha ┆ t_stat ┆ r_squared ┆ n_obs │
# ├──────────┼─────────┼─────────┼────────┼───────────┼───────┤
# │ A00 ┆ 0.082 ┆ 0.0001 ┆ 1.91 ┆ 0.018 ┆ 494 │
# │ ... ┆ ... ┆ ... ┆ ... ┆ ... ┆ ... │
# └──────────┴─────────┴─────────┴────────┴───────────┴───────┘
out = ts_beta(betas_df)
r2 = mean_r_squared(betas_df)
print(out.value, out.stat, out.metadata["p_value"], r2.value)
# 0.078 6.84 4.1e-09 0.021 (approximate)
See also¶
-
trend/oos
Pipe
compute_rolling_mean_betainto the series diagnostics for \(\beta\)-stability and OOS-survival reads. -
by_slice
Axis-agnostic slice dispatcher for per-slice \(\beta\) summaries.
-
Timeseries-mode conventions
Stambaugh bias, plain stage-1 SE rationale, augmented Dickey-Fuller (ADF) persistence discipline.
-
Statistical methods
Cross-asset \(t\), the BJS aggregation pattern, and unit-root discipline on the common factor.
-
Metric applicability reference
When this metric applies and the sample-size guards that gate it (
MIN_TS_OBS, \(N \geq 3\) for cross-asset \(t\), \(N \geq 2\) for sign consistency). -
Common × Continuous landing
Adjacent macro-common metrics in the same cell.