factrix.metrics.common_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_common_betas: per-asset full-sample TS regression → {factor: per-asset DataFrame}.
common_beta: cross-sectional test on the β distribution.
common_beta_r_squared: average explanatory power across assets.
compute_rolling_common_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.common_beta.common_beta ¶
common_beta(common_betas_df: DataFrame) -> MetricResult
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: the headline null is over the cross-asset beta distribution,
not the per-asset time-series slope estimates. The upstream
compute_common_betas fits each asset on its full pairwise-complete
forward-return series; when forward_periods > 1 those returns may
overlap, but only the resulting beta vector enters this cross-asset
test. A strong latent common factor can still link betas across assets.
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_common_betas output:
>>> import factrix as fx
>>> from factrix.preprocess import compute_forward_return
>>> from factrix.metrics.common_beta import compute_common_betas, common_beta
>>> panel = compute_forward_return(
... fx.datasets.make_cs_panel(n_assets=80, n_dates=180, seed=0),
... forward_periods=5,
... )
>>> common_betas_df = compute_common_betas(panel)["factor"]
>>> result = common_beta(common_betas_df)
>>> result.name == ""
True
factrix.metrics.common_beta.common_beta_profile ¶
common_beta_profile(common_betas_df: DataFrame, *, neutral_epsilon: float = EPSILON) -> MetricResult
Descriptive sign and dispersion profile of per-asset common-factor betas.
value is the positive-minus-negative beta mean spread when both sides
exist; otherwise it is NaN and the side counts in metadata explain why.
No hypothesis test is run.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
common_betas_df
|
DataFrame
|
Per-asset beta table from
:func: |
required |
neutral_epsilon
|
float
|
Absolute-beta threshold treated as neutral. Betas with
|
EPSILON
|
Returns:
| Type | Description |
|---|---|
MetricResult
|
MetricResult with descriptive beta-profile metadata and |
MetricResult
|
|
factrix.metrics.common_beta.common_beta_r_squared ¶
common_beta_r_squared(common_betas_df: DataFrame) -> MetricResult
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_common_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_common_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 density the factor explains a small subset of assets rather than the cross-section as a whole.
Examples:
Chain from :func:compute_common_betas output:
>>> import factrix as fx
>>> from factrix.preprocess import compute_forward_return
>>> from factrix.metrics.common_beta import compute_common_betas, common_beta_r_squared
>>> panel = compute_forward_return(
... fx.datasets.make_cs_panel(n_assets=80, n_dates=180, seed=0),
... forward_periods=5,
... )
>>> common_betas_df = compute_common_betas(panel)["factor"]
>>> result = common_beta_r_squared(common_betas_df)
>>> result.name == ""
True
factrix.metrics.common_beta.common_beta_sign_consistency ¶
common_beta_sign_consistency(common_betas_df: DataFrame) -> MetricResult
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
fm_beta.fm_beta_sign_consistency this is direction-agnostic
— it does not require a prior on the factor's expected sign.
Requires n_assets >= 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_assets >= 2 so a single-asset
max(pos, 1-pos) = 1.0 cannot leak into downstream
inference as spurious "perfect agreement". Pair with
fm_beta.fm_beta_sign_consistency when a directional prior
is available.
Examples:
Chain from :func:compute_common_betas output:
>>> import factrix as fx
>>> from factrix.preprocess import compute_forward_return
>>> from factrix.metrics.common_beta import (
... compute_common_betas,
... common_beta_sign_consistency,
... )
>>> panel = compute_forward_return(
... fx.datasets.make_cs_panel(n_assets=80, n_dates=180, seed=0),
... forward_periods=5,
... )
>>> common_betas_df = compute_common_betas(panel)["factor"]
>>> result = common_beta_sign_consistency(common_betas_df)
>>> result.name == ""
True
factrix.metrics.common_beta.compute_rolling_common_beta ¶
compute_rolling_common_beta(data: 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.common_beta import compute_rolling_common_beta
>>> panel = compute_forward_return(
... fx.datasets.make_cs_panel(n_assets=80, n_dates=180, seed=0),
... forward_periods=5,
... )
>>> rolling = compute_rolling_common_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. Non-overlap resampling is not applied (stage-1 runs on the full overlapping series). 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
common_beta,common_beta_profile,common_beta_r_squared, andcommon_beta_sign_consistency. Assets with fewer thanMIN_COMMON_BETA_PERIODS_HARDrows 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 headline null is over the cross-asset beta distribution, not the per-asset time-series slope SEs. A strong latent common factor can still link betas across assets.
metadata["beta_std"]andmetadata["median_beta"]are reported so asset-allocation users can see whether offsetting positive and negative betas are cancelling the average. -
Explanatory power across the cross-section
common_beta_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. -
Heterogeneous beta profiles
A common macro factor can separate asset classes even when its average beta is close to zero. Pair
compute_common_betaswithcommon_beta_profileandcommon_beta_sign_consistencywhen equities, duration, commodities, or currencies may load with opposite signs. -
Rolling-window stability
compute_rolling_common_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_common_betas |
| Mean-\(\beta\) significance across assets (Stage 2 of BJS) | common_beta |
| Positive / negative / neutral beta profile and sign spread | common_beta_profile |
| Average explanatory power \(\overline{R^2}\) across assets | common_beta_r_squared |
| Direction-agnostic sign agreement on per-asset \(\beta\) | common_beta_sign_consistency |
| Rolling cross-asset mean \(\beta\) series for trend / out-of-sample (OOS) pipes | compute_rolling_common_beta |
Allocation reading¶
For asset-allocation panels, read common_beta as the average exposure test, not
as the whole common-factor story. A non-significant mean beta can still sit on
top of a useful rotation profile when one group of assets has positive beta and
another has negative beta.
| Signal in the output | What to inspect |
|---|---|
| Mean beta is near zero, but the factor seems economically relevant | common_beta_profile.metadata["beta_std"], compute_common_betas(...)[factor]["beta"] |
| Betas split between positive and negative assets | common_beta_profile.metadata["n_positive_beta"], metadata["n_negative_beta"], metadata["positive_minus_negative_beta_spread"] |
| A few assets drive the common-factor fit | common_beta_r_squared.value, metadata["median_r_squared"] |
| The factor matters only in high / low states | common_quantile_spread |
These are diagnostics for factor validation. Turning the beta profile into weights, hedges, leverage, or rebalance rules belongs in the downstream portfolio/backtest layer.
Group-aware beta profile¶
common_beta_profile summarizes the whole beta vector. When the research
question is about asset classes, regions, or sectors, join user-supplied labels
to the compute_common_betas(...)[factor] table and summarize the same sign
and dispersion fields by group. Missing labels should be named explicitly
rather than silently dropped. For guidance on matching metrics to allocation
signals, see Validating allocation signals.
import polars as pl
from factrix.metrics.common_beta import compute_common_betas
betas_df = compute_common_betas(panel, factor_cols=["macro_growth"])["macro_growth"]
asset_groups = pl.DataFrame(
{
"asset_id": ["A00", "A01", "A02"],
"asset_group": ["equity", "duration", "commodity"],
}
)
grouped = (
betas_df.join(asset_groups, on="asset_id", how="left")
.with_columns(pl.col("asset_group").fill_null("__missing_group__"))
.group_by("asset_group")
.agg(
pl.len().alias("n_assets"),
(pl.col("beta") > 0).sum().alias("n_positive_beta"),
(pl.col("beta") < 0).sum().alias("n_negative_beta"),
pl.col("beta").mean().alias("beta_mean"),
pl.col("beta").std().alias("beta_std"),
pl.col("beta").abs().mean().alias("abs_beta_mean"),
(
pl.col("beta").filter(pl.col("beta") > 0).mean()
- pl.col("beta").filter(pl.col("beta") < 0).mean()
).alias("positive_minus_negative_beta_spread"),
)
)
The grouped table is a diagnostic read of beta heterogeneity, not a portfolio allocation rule.
Worked example — per-asset TS betas then cross-asset \(t\)¶
compute_common_betas → common_beta on a broadcast common-factor panel
import factrix as fx
import polars as pl
from factrix.metrics.common_beta import compute_common_betas, common_beta, common_beta_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_common_betas(panel)["factor"]
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 = common_beta(betas_df)
r2 = common_beta_r_squared(betas_df)
print(out.value, out.stat, out.p_value, r2.value)
# 0.078 6.84 4.1e-09 0.021 (approximate)
See also¶
-
trend/oos
Pipe
compute_rolling_common_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_COMMON_BETA_PERIODS_HARD,n_assets >= 3for cross-asset t-stat,n_assets >= 2for sign consistency). -
Common × Continuous landing
Adjacent macro-common metrics in the same cell.