diff --git a/playground/grouper.html b/playground/grouper.html new file mode 100644 index 00000000..da23e240 --- /dev/null +++ b/playground/grouper.html @@ -0,0 +1,254 @@ + + + + + + tsb — Grouper + + + + +
+
+
Initializing playground…
+
+ + ← Back to roadmap +

pd.Grouper

+

Grouper is a specification object that encapsulates groupby parameters — mirrors pandas.Grouper.

+ +
+

1 — Key vs Level grouping

+

Create a Grouper for column-key grouping or index-level grouping.

+
+
+ TypeScript +
+ + +
+
+ +
Click ▶ Run to execute
+
Ctrl+Enter to run · Tab to indent
+
+
+ +
+

2 — Options & toString

+

Full set of Grouper options: freq, sort, dropna, closed, label.

+
+
+ TypeScript +
+ + +
+
+ +
Click ▶ Run to execute
+
Ctrl+Enter to run · Tab to indent
+
+
+ +
+

3 — Usage with groupby

+

Use g.key! to pass the key directly to groupby(). Full Grouper integration (freq/level) is a future iteration.

+
+
+ TypeScript +
+ + +
+
+ +
Click ▶ Run to execute
+
Ctrl+Enter to run · Tab to indent
+
+
+ + + + + + diff --git a/playground/hash_array_itertuples.html b/playground/hash_array_itertuples.html new file mode 100644 index 00000000..6d3073f0 --- /dev/null +++ b/playground/hash_array_itertuples.html @@ -0,0 +1,328 @@ + + + + + + tsb — hashArray / itertuples / Series.items Playground + + + + + +
+
+
Initializing playground…
+
+ + ← Back to roadmap +

🔢 hashArray / itertuples / Series.items — Interactive Playground

+

+ Utility hashing and row-iteration APIs — mirrors + pandas.util.hash_array, DataFrame.itertuples(), + and Series.items().
+ Edit any code block below and press ▶ Run + (or Ctrl+Enter) to execute it live in your browser. +

+ + +
+

1 · hashArray

+

+ Hash an array of scalar values element-wise using FNV-1a 64-bit. + Identical inputs always produce the same hash value. +

+
+
+ TypeScript +
+ + +
+
+
import { hashArray } from "tsb";
+
+const arr = [1, "hello", null, true, 42];
+const hashes = hashArray(arr);
+console.log("Hashes:", hashes);
+
+// Duplicate inputs get the same hash
+const h2 = hashArray(["a", "b", "a"]);
+console.log("h2[0] === h2[2]:", h2[0] === h2[2]);
+
Click ▶ Run to execute
+
Ctrl+Enter to run
+
+
+ + +
+

2 · Series.items() / iteritems()

+

+ Iterate over (label, value) pairs from a Series. + iteritems() is an alias for compatibility. +

+
+
+ TypeScript +
+ + +
+
+
import { Series } from "tsb";
+
+const s = new Series({ data: [10, 20, 30], index: ["a", "b", "c"] });
+for (const [label, value] of s.items()) {
+  console.log(label, "→", value);
+}
+
+// iteritems() is an alias
+console.log("\nvia iteritems:");
+console.log([...s.iteritems()]);
+
Click ▶ Run to execute
+
Ctrl+Enter to run
+
+
+ + +
+

3 · DataFrame.itertuples()

+

+ Iterate over rows as plain objects with an Index field. + Pass false to omit the index from each row object. +

+
+
+ TypeScript +
+ + +
+
+
import { DataFrame } from "tsb";
+
+const df = new DataFrame({
+  name:  ["Alice", "Bob", "Carol"],
+  score: [95, 87, 92],
+});
+for (const row of df.itertuples()) {
+  console.log(row);
+}
+
+console.log("\nWithout index:");
+console.log([...df.itertuples(false)]);
+
Click ▶ Run to execute
+
Ctrl+Enter to run
+
+
+ + +
+

🧪 Scratch Pad

+

Write your own code using hashArray, Series.items(), + or DataFrame.itertuples(). All exports from tsb are available.

+
+
+ TypeScript — Scratch Pad +
+ + +
+
+
import { hashArray, Series, DataFrame } from "tsb";
+
+// Combine: hash the values from a Series
+const s = new Series({ data: ["foo", "bar", "foo"], index: [0, 1, 2] });
+const vals = [...s.items()].map(([, v]) => v);
+const hashes = hashArray(vals);
+console.log("foo===foo:", hashes[0] === hashes[2]);
+console.log("foo===bar:", hashes[0] === hashes[1]);
+
Click ▶ Run to execute
+
Ctrl+Enter to run
+
+
+ + + + + + + diff --git a/playground/index.html b/playground/index.html index 27ccedd2..23461661 100644 --- a/playground/index.html +++ b/playground/index.html @@ -284,6 +284,16 @@

✅ Complete +
+

🪟 Window Indexers

+

Custom window indexers for rolling computations: BaseIndexer (abstract base), FixedForwardWindowIndexer (forward-looking N-row window), VariableOffsetWindowIndexer (per-row variable depth), and applyIndexer() helper. Mirrors pandas.api.indexers.

+
✅ Complete
+
+
+

🗺️ Series.map()

+

Map Series values using a function, Record/dict, another Series (index-label lookup), or ES6 Map. Missing keys produce null. Optional naAction: "ignore" passes NA values through unchanged. Mirrors pandas.Series.map().

+
✅ Complete
+

🎭 where / mask

Element-wise conditional selection: seriesWhere / seriesMask and dataFrameWhere / dataFrameMask. Accepts boolean arrays, label-aligned boolean Series/DataFrame, or callables. Mirrors pandas.Series.where, pandas.DataFrame.where, and their .mask() inverses.

diff --git a/playground/series-map.html b/playground/series-map.html new file mode 100644 index 00000000..171809b1 --- /dev/null +++ b/playground/series-map.html @@ -0,0 +1,281 @@ + + + + + + tsb — Series.map() + + + + +
+
+
Initializing playground…
+
+ + ← Back to roadmap +

Series.map()

+

Map values using a function, Record, Series, or ES6 Map — mirrors pandas.Series.map.

+ +
+

1 — Function mapper

+

Apply a function (value, index, pos) => U to every element.

+
+
+ TypeScript +
+ + +
+
+ +
Click ▶ Run to execute
+
Ctrl+Enter to run · Tab to indent
+
+
+ +
+

2 — Record / dict mapper

+

Look up each value (stringified) in a plain JS object. Missing keys produce null.

+
+
+ TypeScript +
+ + +
+
+ +
Click ▶ Run to execute
+
Ctrl+Enter to run · Tab to indent
+
+
+ +
+

3 — Series mapper

+

Look up each value by label in another Series. Missing labels produce null.

+
+
+ TypeScript +
+ + +
+
+ +
Click ▶ Run to execute
+
Ctrl+Enter to run · Tab to indent
+
+
+ +
+

4 — ES6 Map mapper

+

Use a native Map<Scalar, U> for non-string keys (numbers, booleans, null, etc.).

+
+
+ TypeScript +
+ + +
+
+ +
Click ▶ Run to execute
+
Ctrl+Enter to run · Tab to indent
+
+
+ +
+

5 — naAction: "ignore"

+

Pass { naAction: "ignore" } to preserve existing null/NaN values without looking them up.

+
+
+ TypeScript +
+ + +
+
+ +
Click ▶ Run to execute
+
Ctrl+Enter to run · Tab to indent
+
+
+ + + + + + diff --git a/playground/window_indexers.html b/playground/window_indexers.html new file mode 100644 index 00000000..61aa43f5 --- /dev/null +++ b/playground/window_indexers.html @@ -0,0 +1,254 @@ + + + + + + tsb — Window Indexers + + + + +
+
+
Initializing playground…
+
+ + ← Back to roadmap +

🪟 Window Indexers

+

Custom window indexers let you define arbitrary window shapes for rolling computations — mirrors pandas.api.indexers.

+ +
+

1 — FixedForwardWindowIndexer

+

The default rolling looks backward. FixedForwardWindowIndexer looks forward — each row's window covers the next N rows.

+
+
+ TypeScript +
+ + +
+
+ +
Click ▶ Run to execute
+
Ctrl+Enter to run · Tab to indent
+
+
+ +
+

2 — VariableOffsetWindowIndexer

+

Define a different look-back (or look-forward) depth for each row. Useful for event-driven windows or irregular-frequency time series.

+
+
+ TypeScript +
+ + +
+
+ +
Click ▶ Run to execute
+
Ctrl+Enter to run · Tab to indent
+
+
+ +
+

3 — Custom BaseIndexer subclass

+

Subclass BaseIndexer to implement any window shape you need.

+
+
+ TypeScript +
+ + +
+
+ +
Click ▶ Run to execute
+
Ctrl+Enter to run · Tab to indent
+
+
+ + + + + + diff --git a/src/core/frame.ts b/src/core/frame.ts index ddb641a1..ec18d144 100644 --- a/src/core/frame.ts +++ b/src/core/frame.ts @@ -19,12 +19,12 @@ import { DataFrameGroupBy } from "../groupby/index.ts"; import type { Label, Scalar } from "../types.ts"; -import { EWM } from "../window/ewm.ts"; -import type { EwmOptions } from "../window/ewm.ts"; -import { Expanding } from "../window/expanding.ts"; -import type { ExpandingOptions } from "../window/expanding.ts"; -import { Rolling } from "../window/rolling.ts"; -import type { RollingOptions } from "../window/rolling.ts"; +import { EWM } from "../window/index.ts"; +import type { EwmOptions } from "../window/index.ts"; +import { Expanding } from "../window/index.ts"; +import type { ExpandingOptions } from "../window/index.ts"; +import { Rolling } from "../window/index.ts"; +import type { RollingOptions } from "../window/index.ts"; import { Index } from "./base-index.ts"; import { RangeIndex } from "./range-index.ts"; import { Series } from "./series.ts"; @@ -581,6 +581,43 @@ export class DataFrame { yield* this._columns.entries(); } + /** + * Iterate over DataFrame rows as objects with an `Index` property plus + * one property per column — mirrors `DataFrame.itertuples()`. + * + * Unlike pandas' named-tuples (which are positional), JavaScript does not + * have positional tuples with named fields, so each row is returned as a + * plain object `{ Index: label, col1: val, col2: val, ... }`. The `Index` + * key matches the pandas `itertuples(name="Pandas")` default. + * + * @param index - Whether to include the row index as the `Index` field. + * Default `true`. + * @param name - Unused (kept for API parity); TypeScript records have no + * class name. + * + * @example + * ```ts + * const df = new DataFrame({ a: [1, 2], b: ["x", "y"] }); + * for (const row of df.itertuples()) { + * console.log(row); // { Index: 0, a: 1, b: "x" } then { Index: 1, a: 2, b: "y" } + * } + * ``` + */ + *itertuples(index = true, _name?: string): IterableIterator> { + const nRows = this.index.size; + const colNames = this.columns.values as readonly string[]; + for (let i = 0; i < nRows; i++) { + const row: Record = {}; + if (index) { + row["Index"] = this.index.at(i) as Scalar; + } + for (const name of colNames) { + row[name] = this.col(name).iat(i); + } + yield row; + } + } + /** Iterate over `(rowLabel, rowSeries)` pairs — mirrors `DataFrame.iterrows()`. */ *iterrows(): IterableIterator<[Label, Series]> { const nRows = this.index.size; diff --git a/src/core/series.ts b/src/core/series.ts index 2410e13c..59301f20 100644 --- a/src/core/series.ts +++ b/src/core/series.ts @@ -1049,11 +1049,109 @@ export class Series { } /** - * Apply a function to each value, returning a new Series. + * Apply a mapper to each value, returning a new Series. + * + * Three forms are supported (mirroring `pandas.Series.map`): + * + * 1. **Function** `(value, index, pos) => U` — called for every element. + * 2. **Plain object / Record** `{ [key: string]: U }` — each value is looked + * up by its string representation; missing keys map to `null`. + * 3. **Series mapper** — each value is looked up in the mapper Series by + * index label; missing labels map to `null`. + * 4. **ES6 Map** — direct `Map` lookup; missing keys map to `null`. + * + * When `naAction` is `"ignore"`, existing `null`/`undefined`/`NaN` values + * are passed through unchanged instead of being looked up in the mapper. + * The option is only meaningful for dict/Series/Map forms. + * + * @example + * ```ts + * import { Series } from "tsb"; + * const s = new Series({ data: [1, 2, 3], name: "x" }); + * s.map(v => v * 10); // [10, 20, 30] + * s.map({ "1": "one", "2": "two" }); // ["one", "two", null] + * ``` */ - map(fn: (value: T, index: Label, pos: number) => U): Series { - return new Series({ - data: this._values.map((v, i) => fn(v, this.index.at(i), i)), + map(fn: (value: T, index: Label, pos: number) => U): Series; + map( + mapper: Record, + options?: { naAction?: "ignore" | null }, + ): Series; + map( + mapper: Series, + options?: { naAction?: "ignore" | null }, + ): Series; + map( + mapper: Map, + options?: { naAction?: "ignore" | null }, + ): Series; + map( + mapperOrFn: + | ((value: T, index: Label, pos: number) => U) + | Record + | Series + | Map, + options?: { naAction?: "ignore" | null }, + ): Series | Series { + const naAction = options?.naAction ?? null; + + if (typeof mapperOrFn === "function") { + return new Series({ + data: this._values.map((v, i) => (mapperOrFn as (value: T, index: Label, pos: number) => U)(v, this.index.at(i), i)), + index: this.index, + name: this.name, + }); + } + + // Helper: is a value "NA" for the purposes of naAction + const isNa = (v: Scalar): boolean => + v === null || v === undefined || (typeof v === "number" && Number.isNaN(v)); + + if (mapperOrFn instanceof Map) { + const m = mapperOrFn as Map; + const data = this._values.map((v): U | null => { + if (naAction === "ignore" && isNa(v)) { + return v as unknown as U | null; + } + const result = m.get(v); + return result !== undefined ? result : null; + }); + return new Series({ + data, + index: this.index, + name: this.name, + }); + } + + if (mapperOrFn instanceof Series) { + const seriesMapper = mapperOrFn as Series; + const data = this._values.map((v): U | null => { + if (naAction === "ignore" && isNa(v)) { + return v as unknown as U | null; + } + if (!seriesMapper.index.contains(v as Label)) { + return null; + } + return seriesMapper.loc(v as Label) as U | null; + }); + return new Series({ + data, + index: this.index, + name: this.name, + }); + } + + // Plain object / Record + const rec = mapperOrFn as Record; + const data = this._values.map((v): U | null => { + if (naAction === "ignore" && isNa(v)) { + return v as unknown as U | null; + } + const key = String(v); + return Object.prototype.hasOwnProperty.call(rec, key) ? (rec[key] as U) : null; + }); + return new Series({ + data, index: this.index, name: this.name, }); @@ -1180,6 +1278,34 @@ export class Series { groupby(by: readonly Scalar[] | Series): SeriesGroupBy { return new SeriesGroupBy(this as Series, by); } + + // ─── items / iteritems ──────────────────────────────────────────────────── + + /** + * Lazily iterate over `(label, value)` pairs — mirrors `Series.items()`. + * + * @example + * ```ts + * const s = new Series({ data: [10, 20], index: ["a", "b"] }); + * for (const [label, value] of s.items()) { + * console.log(label, value); // "a" 10 then "b" 20 + * } + * ``` + */ + *items(): IterableIterator<[Label, T]> { + const n = this.index.size; + for (let i = 0; i < n; i++) { + yield [this.index.at(i) as Label, this.iat(i) as T]; + } + } + + /** + * Alias for `items()` — mirrors `Series.iteritems()` (deprecated in pandas + * 1.5 but still widely used). + */ + *iteritems(): IterableIterator<[Label, T]> { + yield* this.items(); + } } function isIndexLike(v: unknown): v is Index