Skip to content

factrix.metrics.predictive_beta

Single-asset predictive regression beta.

Notes

Pipeline. One asset, dense factor, time-series OLS with Newey-West heteroskedasticity-and-autocorrelation-consistent (HAC) standard error on the slope.

Input. Long panel with date, asset_id, factor, forward_return where asset_id has one unique value.

Output. MetricResult.value is the predictive slope beta and p_value tests H0: beta = 0.

factrix.metrics.predictive_beta.predictive_beta

predictive_beta(data: DataFrame, *, newey_west_lags: int | None = None, forward_periods: int = 5, adf_threshold: float | None = 0.1, factor_col: str = 'factor', return_col: str = 'forward_return') -> MetricResult

Predictive beta for a single-asset dense factor.

Fits the direct predictive regression \(R_{t+h} = \alpha + \beta F_t + \varepsilon_t\) on one asset and tests H0: beta = 0 with a Newey-West HAC standard error. The Bartlett lag defaults to the Newey-West (1994) automatic rule, floored at forward_periods - 1 so overlapping forward-return windows do not understate the standard error.

Parameters:

Name Type Description Default
data DataFrame

Single-asset long panel with date, asset_id, factor_col and return_col.

required
newey_west_lags int | None

Optional explicit Bartlett lag. None uses the project default bandwidth.

None
forward_periods int

Forward-return horizon injected by evaluate from the panel metadata; standalone calls may pass it directly.

5
adf_threshold float | None

Augmented Dickey-Fuller p-value above which the factor is flagged as persistent. None disables the check.

0.1
factor_col str

Predictor column.

'factor'
return_col str

Forward-return column.

'forward_return'

Returns:

Type Description
MetricResult

MetricResult with value = beta, stat = HAC t statistic,

MetricResult

and p_value for H0: beta = 0.

Notes

This is not a common_beta fallback. common_beta tests the cross-asset mean of per-asset betas and therefore remains a PANEL metric. predictive_beta is the explicit TIMESERIES dense metric for a single asset.


Use cases

  • Single-asset dense predictive slope


    predictive_beta is the (*, DENSE, TIMESERIES) metric for a one-asset panel. It fits forward_return ~ factor and tests whether the slope differs from zero.

  • Not a common_beta fallback


    common_beta tests the cross-asset mean of per-asset betas and remains PANEL-only. predictive_beta is the explicit single-series predictive regression.

  • HAC inference for overlapping returns


    The slope test uses Newey-West HAC covariance. The lag defaults to the Newey-West automatic bandwidth, floored at forward_periods - 1 so overlapping forward-return windows do not understate standard errors.

  • Persistent predictor diagnostic


    The metric also runs a lightweight ADF check on the factor series. When adf_p exceeds adf_threshold, metadata sets unit_root_suspected=True and the result carries WarningCode.PERSISTENT_REGRESSOR. The beta is still returned; the warning tells you to read the slope as a persistent-regressor risk, not as an automatically corrected estimate.

  • Stability is a workflow


    predictive_beta returns the full-sample HAC slope test. Rolling or expanding beta checks should be treated as descriptive stability diagnostics with pre-declared windows, not as a second family of p-values to rank or feed into multiple-testing correction.

Worked example

single-asset panel -> predictive_beta

import polars as pl
import factrix as fx
from factrix.metrics import predictive_beta
from factrix.preprocess import compute_forward_return

raw = fx.datasets.make_cs_panel(n_assets=4, n_dates=180, seed=0)
asset = raw["asset_id"].unique().sort()[0]
panel = compute_forward_return(
    raw.filter(pl.col("asset_id") == asset),
    forward_periods=5,
)

out = fx.evaluate(
    panel,
    metrics={"predictive_beta": predictive_beta()},
    factor_cols=["factor"],
    forward_periods=5,
)["factor"].metrics["predictive_beta"]

print(out.value, out.stat, out.p_value, out.metadata["unit_root_suspected"])

Stability workflow

For a single-asset dense factor, the first question is still the full-sample HAC slope: does factor_t have a statistically legible relation to forward_return_{t+h}? Stability checks come after that and should answer a different question: whether the slope is broadly persistent through time or mostly supported by one segment of the sample.

Use pre-declared windows and read the rolling betas descriptively. The windowed calls below reuse the same predictive_beta estimator, but the resulting t_stat / p_value values are not independent tests because overlapping windows share observations.

rolling beta series as a diagnostic

import polars as pl
import factrix as fx
from factrix.metrics import directional_hit_rate, predictive_beta
from factrix.preprocess import compute_forward_return

raw = fx.datasets.make_cs_panel(n_assets=4, n_dates=180, seed=0)
asset = raw["asset_id"].unique().sort()[0]
panel = compute_forward_return(
    raw.filter(pl.col("asset_id") == asset),
    forward_periods=5,
)

full = predictive_beta(panel, forward_periods=5)
hit = directional_hit_rate(panel, forward_periods=5)

dates = panel.select("date").unique().sort("date")["date"].to_list()
window_periods = 60
step_periods = 20
rows = []
for end_idx in range(window_periods, len(dates) + 1, step_periods):
    window_dates = dates[end_idx - window_periods : end_idx]
    window = panel.filter(
        pl.col("date").is_between(window_dates[0], window_dates[-1])
    )
    result = predictive_beta(window, forward_periods=5)
    rows.append(
        {
            "date": window_dates[-1],
            "window_start": window_dates[0],
            "window_end": window_dates[-1],
            "beta": result.value,
            "t_stat": result.stat,
            "n_periods": result.metadata["n_periods"],
            "r_squared": result.metadata["r_squared"],
        }
    )

beta_series = pl.DataFrame(rows)
reference_sign = 1.0 if full.value >= 0.0 else -1.0
stability = beta_series.select(
    ((pl.col("beta") * reference_sign) > 0).mean().alias("sign_consistency"),
    pl.col("beta").median().alias("median_beta"),
    pl.col("beta").last().alias("recent_beta"),
    pl.col("beta").min().alias("min_beta"),
    pl.col("beta").max().alias("max_beta"),
)

print("full beta:", full.value, "p:", full.p_value)
print("directional hit:", hit.value, "p:", hit.p_value)
print(stability)

Read this output as a stability profile:

  • full.value / full.p_value is the canonical single-asset dense inference.
  • directional_hit_rate checks whether the factor gets the return sign right.
  • sign_consistency asks how often rolling betas keep the full-sample sign.
  • recent_beta, median_beta, min_beta, and max_beta show whether the signal is decaying, flipping, or concentrated in one segment.

Avoid turning this workflow into automatic model selection. Do not choose the window after looking at the strongest beta, do not feed overlapping-window p_value values into bhy, and do not interpret sign_consistency as a formal structural-break test. If the stability profile points to a regime change, split that regime hypothesis explicitly and test it as a separate research design.