CODE · NUMBA ROLLING OPERATORS

Fast Rolling Mean, Std, Beta and Z-score with Numba

High-Performance Python · Snippet ID: rolling-window-numba

What it does. These functions compute rolling statistics with running sums in a single compiled pass, instead of recomputing each window. Values are re-centred on the first window's mean, which avoids the catastrophic cancellation that running sums suffer on series with large levels, such as prices or index points. Numba compiles plain Python loops to machine code, so there is no build step: install with pip and call the functions on NumPy arrays.

Approach

Running sums over the window, after centring v = x − c:
  S₁ += v_new − v_old       S₂ += v_new² − v_old²
  mean = S₁/w + c            var = (S₂ − w·(S₁/w)²) / (w − 1)
  beta = (S_xy − S_x·S_y/w) / (S_xx − S_x²/w)

Measured here, 1,000,000 points, window 250 (mean + std + beta):  Numba ≈ 20 ms, pandas ≈ 135 ms
2,000 × 500 matrix, rolling z-score, window 60:                   Numba ≈ 11 ms, pandas ≈ 52 ms
Max relative std error vs exact two-pass:                         Numba 1.6e−9, pandas 2.2e−8

When to use it

  • Long minute-bar or tick series where pandas rolling is the bottleneck.
  • Rolling hedge ratios for pairs, and rolling z-scores across a whole stock universe in one call.
  • Inside larger Numba-compiled backtest loops, since these functions can be called from other @njit code.

Pitfalls

  • NaN handling. The inputs must be NaN-free. Running sums propagate a NaN forever. Fill, drop or split around gaps first.
  • Compile time. The first call compiles, which takes about a second. cache=True stores the compiled code next to the file so later runs start instantly.
  • Numerical drift. Without centring, the variance of a series around 10,000 loses most of its significant digits. If the level changes enormously over the sample, re-centre periodically.
  • Parallelism. For very wide matrices, switch the column loop in rolling_zscore_2d to numba.prange with parallel=True.

Python reference implementation

Requires numpy, numba. Tested before publication; the generator re-executes it on every rebuild. MIT licensed. Download rolling-window-numba.py · All snippets

import numpy as np
from numba import njit

@njit(cache=True)
def rolling_mean_std(x, window):
    """O(n) rolling mean and sample std (ddof=1) with running sums.
    Values are re-centred on the first window's mean to limit cancellation error.
    NaN where fewer than `window` observations; input must contain no NaN."""
    n = x.shape[0]
    mean = np.full(n, np.nan)
    std = np.full(n, np.nan)
    if n < window:
        return mean, std
    shift = x[:window].mean()
    s = 0.0
    s2 = 0.0
    for i in range(n):
        v = x[i] - shift
        s += v
        s2 += v * v
        if i >= window:
            o = x[i - window] - shift
            s -= o
            s2 -= o * o
        if i >= window - 1:
            m = s / window
            mean[i] = m + shift
            var = (s2 - window * m * m) / (window - 1)
            std[i] = np.sqrt(var) if var > 0 else 0.0
    return mean, std

@njit(cache=True)
def rolling_beta(y, x, window):
    """Rolling OLS slope cov(y, x) / var(x), O(n). Use for hedge ratios or market beta.
    Inputs are re-centred on their first-window means to limit cancellation error."""
    n = y.shape[0]
    out = np.full(n, np.nan)
    if n < window:
        return out
    cx = x[:window].mean()
    cy = y[:window].mean()
    sx = sy = sxx = sxy = 0.0
    for i in range(n):
        a = x[i] - cx
        b = y[i] - cy
        sx += a; sy += b; sxx += a * a; sxy += a * b
        if i >= window:
            a = x[i - window] - cx
            b = y[i - window] - cy
            sx -= a; sy -= b; sxx -= a * a; sxy -= a * b
        if i >= window - 1:
            cov = sxy - sx * sy / window
            var = sxx - sx * sx / window
            if var > 0:
                out[i] = cov / var
    return out

@njit(cache=True)
def rolling_zscore_2d(X, window):
    """Rolling z-score for every column of a (T x N) float matrix, in one compiled pass."""
    T, N = X.shape
    Z = np.full((T, N), np.nan)
    for j in range(N):
        m, s = rolling_mean_std(X[:, j].copy(), window)
        for i in range(T):
            if s[i] > 0:
                Z[i, j] = (X[i, j] - m[i]) / s[i]
    return Z

FAQ

Why Numba instead of Cython?

Numba needs no compiler toolchain or build step: decorate a function and it is compiled on first call. Performance for simple numeric loops is comparable to Cython, and the code stays plain Python that can be copied and run directly.

Is pandas rolling not already fast?

pandas rolling is implemented in Cython and is fine for most work, but each statistic is a separate pass with object overhead, and combinations such as rolling beta need several calls. A single fused Numba pass was 4–7× faster in the benchmarks above.

Summary for AI agents (Markdown mirror)

# Fast Rolling Mean, Std, Beta and Z-score with Numba

> O(n) rolling mean, standard deviation, OLS beta and 2-D z-score compiled with Numba. In tests here they run 4 to 7 times faster than pandas and are more accurate on large-valued series.

- Category: High-Performance Python

- Canonical URL: https://quantcore-labs.com/code/rolling-window-numba/

- Part of: https://quantcore-labs.com/code/

- Python source: https://quantcore-labs.com/code/rolling-window-numba/rolling-window-numba.py