tsb — Rolling Apply & Multi-Aggregation

Standalone functions for applying custom aggregation logic over sliding windows, mirroring pandas.Series.rolling().apply() and Rolling.agg() .

1. rollingApply — Custom Function Per Window

Apply any aggregation function to each rolling window. The function receives the valid (non-null, non-NaN) numeric values in the window and must return a single number.

import { rollingApply } from "tsb";

const prices = new Series({ data: [10, 12, 11, 15, 14, 16], name: "price" });

// Custom: range (max - min) over each 3-day window
const range = (w) => Math.max(...w) - Math.min(...w);

rollingApply(prices, 3, range).toArray();
// [null, null, 2, 4, 4, 5]
//  ↑↑ insufficient data (need 3 observations)

Options

OptionDefaultDescription
minPeriodswindowMinimum valid observations to compute (null otherwise)
centerfalseCentre the window (symmetric) instead of trailing
rawfalsePass full window including nulls (filtered to valid nums before fn call)
// minPeriods=1 → start computing from the very first position
rollingApply(prices, 3, range, { minPeriods: 1 }).toArray();
// [0, 2, 2, 4, 4, 5]

// center=true → symmetric window around each point
rollingApply(prices, 3, range, { center: true }).toArray();
// [null, 2, 4, 4, 5, null]

2. rollingAgg — Multiple Aggregations at Once

Apply several named aggregation functions in a single pass over a Series, returning a DataFrame where each column holds one aggregation result.

import { rollingAgg } from "tsb";

const s = new Series({ data: [1, 2, 3, 4, 5, 6, 7, 8] });

const result = rollingAgg(s, 3, {
  mean: (w) => w.reduce((a, b) => a + b, 0) / w.length,
  max:  (w) => Math.max(...w),
  min:  (w) => Math.min(...w),
  range:(w) => Math.max(...w) - Math.min(...w),
});

// result is a DataFrame with columns: "mean", "max", "min", "range"
// result.col("mean").toArray() → [null, null, 2, 3, 4, 5, 6, 7]
// result.col("range").toArray() → [null, null, 2, 2, 2, 2, 2, 2]
Pandas equivalent:
s.rolling(3).agg({"mean": np.mean, "max": np.max, "min": np.min})

3. dataFrameRollingApply — Apply Per Column

Apply a single custom function independently to each column of a DataFrame, returning a new DataFrame of the same shape.

import { dataFrameRollingApply } from "tsb";

const df = DataFrame.fromColumns({
  open:  [100, 102, 101, 105, 103],
  close: [101, 103, 100, 106, 104],
});

// Pairwise range within each 2-step window per column
const range = (w) => Math.max(...w) - Math.min(...w);

dataFrameRollingApply(df, 2, range);
//   open  close
// 0 null  null
// 1    2     2
// 2    1     3
// 3    4     6
// 4    2     2

4. dataFrameRollingAgg — Multi-Agg Per Column

Apply multiple named aggregation functions to every column of a DataFrame. The result has columns named {originalColumn}_{aggName}.

import { dataFrameRollingAgg } from "tsb";

const df = DataFrame.fromColumns({
  A: [1, 2, 3, 4, 5],
  B: [10, 20, 30, 40, 50],
});

const out = dataFrameRollingAgg(df, 3, {
  sum:  (w) => w.reduce((a, b) => a + b, 0),
  mean: (w) => w.reduce((a, b) => a + b, 0) / w.length,
});

// Columns: "A_sum", "A_mean", "B_sum", "B_mean"
// A_sum:  [null, null, 6, 9, 12]
// A_mean: [null, null, 2, 3,  4]
// B_sum:  [null, null, 60, 90, 120]
// B_mean: [null, null, 20, 30,  40]

Comparison with pandas

tsbpandas
rollingApply(s, w, fn) s.rolling(w).apply(fn, raw=True)
rollingApply(s, w, fn, {minPeriods:1}) s.rolling(w, min_periods=1).apply(fn)
rollingAgg(s, w, {f1, f2}) s.rolling(w).agg({"f1": f1, "f2": f2})
dataFrameRollingApply(df, w, fn) df.rolling(w).apply(fn)
dataFrameRollingAgg(df, w, {f1, f2}) df.rolling(w).agg({"f1": f1, "f2": f2})

Use case: Bollinger Band width

import { rollingAgg } from "tsb";

// Bollinger Band width = (upper - lower) / middle
// where upper = mean + 2·std, lower = mean - 2·std
const prices = new Series({
  data: [20, 21, 22, 20, 19, 21, 23, 24, 22, 21],
  name: "price",
});

const stats = rollingAgg(prices, 5, {
  mean: (w) => w.reduce((a, b) => a + b, 0) / w.length,
  std: (w) => {
    const m = w.reduce((a, b) => a + b, 0) / w.length;
    return Math.sqrt(w.reduce((a, b) => a + (b - m) ** 2, 0) / (w.length - 1));
  },
});

// Bollinger Band width = 4 * std / mean
const bw = stats.col("std").toArray().map((std, i) => {
  const mean = stats.col("mean").toArray()[i];
  if (std === null || mean === null || mean === 0) return null;
  return (4 * (std as number)) / (mean as number);
});

← Back to tsb playground index