where / mask

Conditional value selection and replacement — mirrors pandas.Series.where and pandas.DataFrame.mask.

1 — whereSeries: keep values where condition is true

whereSeries(series, cond) keeps each element where cond is true and replaces it with null (or a custom other) where cond is false.

import { Series, whereSeries } from "tsb";

const scores = new Series({ data: [45, 72, 58, 88, 91, 30], name: "score" });

// Keep only passing scores (>= 60); replace failing scores with null
const passing = whereSeries(scores, (v) => v >= 60);
console.log("passing:", [...passing.values]);
// → [null, 72, null, 88, 91, null]

// Replace failing scores with 0 instead of null
const zeroFail = whereSeries(scores, (v) => v >= 60, { other: 0 });
console.log("zero-fail:", [...zeroFail.values]);
// → [0, 72, 0, 88, 91, 0]
▶ run

2 — maskSeries: replace values where condition is true

maskSeries is the inverse of whereSeries: it replaces where cond is true and keeps where cond is false.

import { Series, maskSeries } from "tsb";

const temps = new Series({ data: [-5, 12, -3, 20, 7], name: "temp_C" });

// Mask (hide) sub-zero temperatures
const noFrost = maskSeries(temps, (v) => v < 0);
console.log("no frost:", [...noFrost.values]);
// → [null, 12, null, 20, 7]

// Replace sub-zero with a sentinel value
const clamped = maskSeries(temps, (v) => v < 0, { other: 0 });
console.log("clamped: ", [...clamped.values]);
// → [0, 12, 0, 20, 7]
▶ run

3 — Boolean Series as condition

Pass a Series<boolean> (or a plain boolean array) as the condition for position-aligned filtering.

import { Series, whereSeries, maskSeries } from "tsb";

const prices = new Series({ data: [100, 200, 150, 80, 300], name: "price" });
const inStock = new Series({ data: [true, false, true, false, true] });

// Keep prices only for in-stock items
const available = whereSeries(prices, inStock);
console.log("in-stock prices:", [...available.values]);
// → [100, null, 150, null, 300]

// Mask out-of-stock prices (same result — cond is inverted)
const masked = maskSeries(prices, inStock.values.map((v) => !v));
console.log("masked:         ", [...masked.values]);
// → [100, null, 150, null, 300]
▶ run

4 — whereDataFrame: cell-wise filtering on a DataFrame

whereDataFrame(df, cond) applies the condition independently to each cell across all columns.

import { DataFrame, whereDataFrame } from "tsb";

const df = DataFrame.fromColumns({
  a: [1, -2,  3],
  b: [-4,  5, -6],
  c: [ 7,  8,  9],
});

// Keep non-negative values; replace negatives with null
const positive = whereDataFrame(df, (v) => v >= 0);
console.log("a:", [...positive.col("a").values]); // [1,  null, 3]
console.log("b:", [...positive.col("b").values]); // [null, 5, null]
console.log("c:", [...positive.col("c").values]); // [7,  8,  9]
▶ run

5 — maskDataFrame: replace cells matching condition

import { DataFrame, maskDataFrame } from "tsb";

const df = DataFrame.fromColumns({
  revenue: [100, 0, 250, -50, 0],
  cost:    [ 80, 0, 200,  30, 0],
});

// Mask zeros (replace with null to mark as missing)
const noZeros = maskDataFrame(df, (v) => v === 0);
console.log("revenue:", [...noZeros.col("revenue").values]);
// → [100, null, 250, -50, null]
console.log("cost:   ", [...noZeros.col("cost").values]);
// → [80, null, 200, 30, null]
▶ run

6 — DataFrame condition (boolean DataFrame)

Pass a boolean DataFrame as the condition for per-cell control.

import { DataFrame, whereDataFrame } from "tsb";

const data = DataFrame.fromColumns({
  x: [10, 20, 30],
  y: [40, 50, 60],
});

// Custom boolean mask per cell
const cond = DataFrame.fromColumns({
  x: [true,  false, true],
  y: [false,  true, true],
});

const result = whereDataFrame(data, cond);
console.log("x:", [...result.col("x").values]); // [10, null, 30]
console.log("y:", [...result.col("y").values]); // [null, 50, 60]
▶ run

7 — Combining where and mask for range clamping

Chaining whereSeries and maskSeries is a clean way to apply lower and upper bounds.

import { Series, whereSeries, maskSeries } from "tsb";

const raw = new Series({ data: [-10, 0, 5, 15, 100, 3], name: "value" });
const LO = 0, HI = 10;

// 1) Replace values below lower bound with LO
const step1 = whereSeries(raw, (v) => (v as number) >= LO, { other: LO });
// 2) Replace values above upper bound with HI
const clamped = whereSeries(step1, (v) => (v as number) <= HI, { other: HI });

console.log("clamped:", [...clamped.values]);
// → [0, 0, 5, 10, 10, 3]
▶ run

8 — where / mask vs. clip

When to use which?
Use clip() for simple numeric lower/upper bounds.
Use where() / mask() for arbitrary conditions — including non-numeric types, string patterns, or per-cell boolean DataFrames.
import { Series, whereSeries, clip } from "tsb";

const s = new Series({ data: [-3, 1, 5, 10], name: "val" });

// clip is concise for numeric bounds
const clipped  = clip(s, { lower: 0, upper: 6 });
console.log("clipped: ", [...clipped.values]);  // [0, 1, 5, 6]

// where gives full control — replace out-of-range with null instead of clamping
const filtered = whereSeries(s, (v) => (v as number) >= 0 && (v as number) <= 6);
console.log("filtered:", [...filtered.values]); // [null, 1, 5, null]
▶ run