regression.estimate_betas()

Estimate rolling betas.

Usage

Source

regression.estimate_betas(
    data,
    model,
    lookback,
    min_obs=None,
    id_col="permno",
)

Estimates rolling betas for a given model using the provided data. For each stock, the regression specified by ‘model’ is fit over a rolling calendar window of length ‘lookback’ (e.g. ‘“60mo”’ for sixty months).

The estimator avoids refitting a full regression for every window. Instead it accumulates the per-observation cross-products that define the normal equations (the design Gram matrix ‘X’X’ and the moment vector ‘X’y’), takes their rolling sums via cumulative-sum differencing, and solves the resulting small linear system once per window. This closed-form approach follows the fast beta estimation described at https://www.tidy-finance.org/blog/fast-beta-estimation/ and is considerably faster than looping rolling regressions while returning the same coefficients.

Parameters

data: pl.DataFrame

Data frame containing the data with a date identifier (defaults to ‘date’), a stock identifier (defaults to ‘permno’), and the other variables used in the model.

model: str

Formula describing the model to be estimated (e.g., ‘ret_excess ~ mkt_excess + hml + smb’). An intercept is included unless the formula ends in ‘- 1’ (or ‘+ 0’).

lookback: str or int

Rolling window length. Pass a duration string — a positive count followed by one of ‘“mo”’ (months), ‘“d”’ (days), ‘“h”’, ‘“m”’, ‘“s”’ — to roll over calendar periods, e.g. ‘“60mo”’. The window for a period ‘v’ spans every observation falling in the ‘lookback’ periods ending at ‘v’, so gaps in a stock’s history consume window space, and one row is returned per stock and period. Sub-day units require a datetime column.

Passing a plain integer selects the legacy behaviour — a window of that many consecutive observations, one output row per input row — and emits a ‘DeprecationWarning’.

min_obs: int = None

Minimum number of observations required to estimate the model. Defaults to ‘round(0.8 * lookback)’.

id_col: str = "permno"
Column name representing the stock identifier.

Returns

pl.DataFrame

Data frame with the estimated betas for each stock and time period. Contains the stock identifier and the ‘date’ column, followed by one column per model term: an ‘intercept’ column (when the model includes one) and one ‘beta_’ column per regressor.

With a calendar ‘lookback’ there is one row per stock and period, ‘date’ is floored to the start of the period, and windows with fewer than ‘min_obs’ observations are dropped from the output. With the deprecated integer ‘lookback’ there is one row per input row, ‘date’ is the observation’s own date, and sub-‘min_obs’ windows yield null coefficients.

Examples

from datetime import date
import numpy as np
import polars as pl
from tidyfinance import estimate_betas
rng = np.random.default_rng(1234)
dates = pl.date_range(
    date(2020, 1, 1), date(2020, 12, 1), "1mo", eager=True
)
data_monthly = pl.DataFrame({
    'date': np.repeat(dates.to_numpy(), 50),
    'permno': np.tile(np.arange(1, 51), 12),
    'ret_excess': rng.normal(0, 0.1, 600),
    'mkt_excess': rng.normal(0, 0.1, 600),
    'smb': rng.normal(0, 0.1, 600),
    'hml': rng.normal(0, 0.1, 600),
})
estimate_betas(data_monthly, 'ret_excess ~ mkt_excess', lookback='3mo')