diff --git a/benchmarks/pandas/bench_boolean_array.py b/benchmarks/pandas/bench_boolean_array.py new file mode 100644 index 00000000..8a16b8d8 --- /dev/null +++ b/benchmarks/pandas/bench_boolean_array.py @@ -0,0 +1,43 @@ +"""Benchmark: BooleanArray — nullable boolean extension array operations. +N=100_000 elements with ~10% nulls using pandas BooleanArray. +Tests: array creation, any, all, sum, and, or, invert, fillna. +""" +import json +import time +import pandas as pd + +N = 100_000 +WARMUP = 5 +ITERATIONS = 50 + +# Same pattern as TS version (~10% nulls) +raw = [(None if i % 10 == 0 else bool(i % 3 != 0)) for i in range(N)] +raw2 = [(None if i % 7 == 0 else bool(i % 2 == 0)) for i in range(N)] + + +def run(): + a = pd.array(raw, dtype="boolean") + b = pd.array(raw2, dtype="boolean") + _ = a.any(skipna=True) + _ = a.all(skipna=True) + _ = a.sum(skipna=True) + _ = a & b + _ = a | b + _ = ~a + _ = a.fillna(False) + + +for _ in range(WARMUP): + run() + +start = time.perf_counter() +for _ in range(ITERATIONS): + run() +total = (time.perf_counter() - start) * 1000 + +print(json.dumps({ + "function": "boolean_array", + "mean_ms": total / ITERATIONS, + "iterations": ITERATIONS, + "total_ms": total, +})) diff --git a/benchmarks/pandas/bench_datetime_array.py b/benchmarks/pandas/bench_datetime_array.py new file mode 100644 index 00000000..139917d6 --- /dev/null +++ b/benchmarks/pandas/bench_datetime_array.py @@ -0,0 +1,41 @@ +"""Benchmark: DatetimeArray — nullable datetime extension array operations. +N=100_000 elements with ~10% nulls using pandas DatetimeArray. +Tests: from_sequence, year, month, day, isna, notna, fillna. +""" +import json +import time +import pandas as pd +import numpy as np + +N = 100_000 +WARMUP = 3 +ITERATIONS = 50 + +base = pd.Timestamp("2020-01-01") +raw = [(None if i % 10 == 0 else base + pd.Timedelta(days=i)) for i in range(N)] + + +def run(): + a = pd.array(raw, dtype="datetime64[ns]") + _ = a.year + _ = a.month + _ = a.day + _ = pd.isna(a) + _ = ~pd.isna(a) + _ = a.fillna(pd.Timestamp("2000-01-01")) + + +for _ in range(WARMUP): + run() + +start = time.perf_counter() +for _ in range(ITERATIONS): + run() +total = (time.perf_counter() - start) * 1000 + +print(json.dumps({ + "function": "datetime_array", + "mean_ms": total / ITERATIONS, + "iterations": ITERATIONS, + "total_ms": total, +})) diff --git a/benchmarks/pandas/bench_first_last_valid_index.py b/benchmarks/pandas/bench_first_last_valid_index.py new file mode 100644 index 00000000..674aab50 --- /dev/null +++ b/benchmarks/pandas/bench_first_last_valid_index.py @@ -0,0 +1,45 @@ +""" +Benchmark: first_valid_index / last_valid_index +Outputs JSON: {"function": "first_last_valid_index", "mean_ms": ..., "iterations": ..., "total_ms": ...} +""" +import json +import time +import numpy as np +import pandas as pd + +N = 100_000 + +# Series where first valid is near the start (a few NaN at beginning) +data_start = np.where(np.arange(N) < 10, np.nan, np.arange(N, dtype=float)) +series_start = pd.Series(data_start) + +# Series where last valid is near the end (a few NaN at the end) +data_end = np.where(np.arange(N) >= N - 10, np.nan, np.arange(N, dtype=float)) +series_end = pd.Series(data_end) + +# Series with NaN scattered throughout +data_mixed = np.where(np.arange(N) % 7 == 0, np.nan, np.arange(N, dtype=float)) +series_mixed = pd.Series(data_mixed) + +# Warm-up +for _ in range(20): + series_start.first_valid_index() + series_end.last_valid_index() + series_mixed.first_valid_index() + series_mixed.last_valid_index() + +iterations = 500 +start = time.perf_counter() +for _ in range(iterations): + series_start.first_valid_index() + series_end.last_valid_index() + series_mixed.first_valid_index() + series_mixed.last_valid_index() +total_ms = (time.perf_counter() - start) * 1000 + +print(json.dumps({ + "function": "first_last_valid_index", + "mean_ms": total_ms / iterations, + "iterations": iterations, + "total_ms": total_ms, +})) diff --git a/benchmarks/pandas/bench_gaussian_kde.py b/benchmarks/pandas/bench_gaussian_kde.py new file mode 100644 index 00000000..5d012c82 --- /dev/null +++ b/benchmarks/pandas/bench_gaussian_kde.py @@ -0,0 +1,48 @@ +"""Benchmark: Gaussian KDE on 10k data points — evaluate, integrate (pure numpy)""" +import json, time +import numpy as np + +N = 10_000 +EVAL_POINTS = 200 +WARMUP = 3 +ITERATIONS = 20 + +# Generate data from a bimodal distribution +indices = np.arange(N, dtype=np.float64) +t = indices / N +data = np.where(t < 0.5, np.sin(indices * 0.05) * 2 + 3, np.cos(indices * 0.03) * 2 - 3) + +eval_pts = np.linspace(-6, -6 + (EVAL_POINTS - 1) * 0.06, EVAL_POINTS) + +# Silverman bandwidth (matches tsb default) +std = np.std(data, ddof=1) +bw = (4.0 / (3.0 * N)) ** 0.2 * std + +SQRT_2PI = np.sqrt(2.0 * np.pi) + +def kde_evaluate(data, eval_pts, bw): + # shape: (n_eval, n_data) + z = (eval_pts[:, None] - data[None, :]) / bw + return np.exp(-0.5 * z * z).sum(axis=1) / (N * bw * SQRT_2PI) + +def kde_integrate(data, a, b, bw, n=200): + xs = np.linspace(a, b, n) + ys = kde_evaluate(data, xs, bw) + return np.trapz(ys, xs) + +for _ in range(WARMUP): + kde_evaluate(data, eval_pts, bw) + kde_integrate(data, -2, 2, bw) + +start = time.perf_counter() +for _ in range(ITERATIONS): + kde_evaluate(data, eval_pts, bw) + kde_integrate(data, -2, 2, bw) +total = (time.perf_counter() - start) * 1000 + +print(json.dumps({ + "function": "gaussian_kde", + "mean_ms": total / ITERATIONS, + "iterations": ITERATIONS, + "total_ms": total, +})) diff --git a/benchmarks/pandas/bench_mode.py b/benchmarks/pandas/bench_mode.py new file mode 100644 index 00000000..cf585756 --- /dev/null +++ b/benchmarks/pandas/bench_mode.py @@ -0,0 +1,27 @@ +"""Benchmark: mode on 100k-element Series (mixed numeric with repeats)""" +import json, time +import numpy as np +import pandas as pd + +ROWS = 100_000 +WARMUP = 3 +ITERATIONS = 10 + +# Same data: values 0..9 cycling so mode is meaningful +data = np.arange(ROWS) % 10 +s = pd.Series(data, dtype="float64") + +for _ in range(WARMUP): + s.mode() + +start = time.perf_counter() +for _ in range(ITERATIONS): + s.mode() +total = (time.perf_counter() - start) * 1000 + +print(json.dumps({ + "function": "mode", + "mean_ms": total / ITERATIONS, + "iterations": ITERATIONS, + "total_ms": total, +})) diff --git a/benchmarks/pandas/bench_string_array.py b/benchmarks/pandas/bench_string_array.py new file mode 100644 index 00000000..fd9bcda2 --- /dev/null +++ b/benchmarks/pandas/bench_string_array.py @@ -0,0 +1,41 @@ +"""Benchmark: StringArray — nullable string extension array operations. +N=100_000 elements with ~10% nulls using pandas StringDtype. +Tests: from_sequence, upper, lower, strip, contains, len, fillna. +""" +import json +import time +import pandas as pd + +N = 100_000 +WARMUP = 3 +ITERATIONS = 50 + +WORDS = ["hello", "world", " foo ", "bar", "baz", " qux ", "quux", "corge", "grault", "garply"] + +raw = [(None if i % 10 == 0 else WORDS[i % len(WORDS)]) for i in range(N)] + + +def run(): + a = pd.array(raw, dtype="string") + _ = a.str.upper() + _ = a.str.lower() + _ = a.str.strip() + _ = a.str.contains("oo", na=False) + _ = a.str.len() + _ = a.fillna("NA") + + +for _ in range(WARMUP): + run() + +start = time.perf_counter() +for _ in range(ITERATIONS): + run() +total = (time.perf_counter() - start) * 1000 + +print(json.dumps({ + "function": "string_array", + "mean_ms": total / ITERATIONS, + "iterations": ITERATIONS, + "total_ms": total, +})) diff --git a/benchmarks/pandas/bench_timedelta_array.py b/benchmarks/pandas/bench_timedelta_array.py new file mode 100644 index 00000000..fb64110f --- /dev/null +++ b/benchmarks/pandas/bench_timedelta_array.py @@ -0,0 +1,58 @@ +""" +Benchmark: pd.arrays.TimedeltaArray — create and operate on nullable timedelta arrays. +Outputs JSON: {"function": "timedelta_array", "mean_ms": ..., "iterations": ..., "total_ms": ...} +""" +import json +import time +import numpy as np +import pandas as pd + +SIZE = 100_000 +WARMUP = 5 +ITERATIONS = 50 + +# Build values: ~10% NaT, small durations (i seconds) to avoid overflow +values = np.array( + [None if i % 10 == 0 else i * 1_000_000_000 for i in range(SIZE)], # nanoseconds (1 ns/unit) + dtype=object, +) +td_values = pd.to_timedelta(values, unit="ns") +fill_value = pd.Timedelta(0) + + +def run(): + arr = pd.array(td_values, dtype="timedelta64[ns]") + + # Component access + _ = arr.days + _ = arr.seconds + _ = arr.total_seconds() + + # Null checks + _ = arr.isna() + _ = ~arr.isna() + + # Aggregation (via numpy) + valid = td_values[~pd.isna(td_values)] + _ = valid.sum() + _ = valid.min() + _ = valid.max() + + # Fill + _ = arr.fillna(fill_value) + + +for _ in range(WARMUP): + run() + +start = time.perf_counter() +for _ in range(ITERATIONS): + run() +total_ms = (time.perf_counter() - start) * 1000 + +print(json.dumps({ + "function": "timedelta_array", + "mean_ms": total_ms / ITERATIONS, + "iterations": ITERATIONS, + "total_ms": total_ms, +})) diff --git a/benchmarks/tsb/bench_boolean_array.ts b/benchmarks/tsb/bench_boolean_array.ts new file mode 100644 index 00000000..9d00600e --- /dev/null +++ b/benchmarks/tsb/bench_boolean_array.ts @@ -0,0 +1,46 @@ +/** + * Benchmark: BooleanArray — nullable boolean extension array operations. + * N=100_000 elements with ~10% nulls. Tests from/any/all/sum/and/or/not/fillna. + */ +import { arrays } from "../../src/index.js"; + +const N = 100_000; +const WARMUP = 5; +const ITERATIONS = 50; + +// Build input with ~10% nulls (same pattern across TS and Python) +const raw: (boolean | null)[] = Array.from({ length: N }, (_, i) => + i % 10 === 0 ? null : i % 3 !== 0, +); + +// Build a second array for bitwise ops +const raw2: (boolean | null)[] = Array.from({ length: N }, (_, i) => + i % 7 === 0 ? null : i % 2 === 0, +); + +function run(): void { + const a = arrays.BooleanArray.from(raw); + const b = arrays.BooleanArray.from(raw2); + a.any(); + a.all(); + a.sum(); + a.and(b); + a.or(b); + a.not(); + a.fillna(false); +} + +for (let i = 0; i < WARMUP; i++) run(); + +const start = performance.now(); +for (let i = 0; i < ITERATIONS; i++) run(); +const total = performance.now() - start; + +console.log( + JSON.stringify({ + function: "boolean_array", + mean_ms: total / ITERATIONS, + iterations: ITERATIONS, + total_ms: total, + }), +); diff --git a/benchmarks/tsb/bench_datetime_array.ts b/benchmarks/tsb/bench_datetime_array.ts new file mode 100644 index 00000000..db6de09e --- /dev/null +++ b/benchmarks/tsb/bench_datetime_array.ts @@ -0,0 +1,41 @@ +/** + * Benchmark: DatetimeArray — nullable datetime extension array operations. + * N=100_000 elements with ~10% nulls. Tests from/year/month/day/isna/notna/fillna. + */ +import { arrays, Timestamp } from "../../src/index.js"; + +const N = 100_000; +const WARMUP = 3; +const ITERATIONS = 50; + +const BASE_MS = new Date("2020-01-01").getTime(); +const raw: (string | null)[] = Array.from({ length: N }, (_, i) => { + if (i % 10 === 0) return null; + const ms = BASE_MS + i * 86_400_000; // 1 day per element + return new Date(ms).toISOString().slice(0, 10); +}); + +function run(): void { + const a = arrays.DatetimeArray.from(raw); + a.year; + a.month; + a.day; + a.isna(); + a.notna(); + a.fillna(new Timestamp("2000-01-01")); +} + +for (let i = 0; i < WARMUP; i++) run(); + +const start = performance.now(); +for (let i = 0; i < ITERATIONS; i++) run(); +const total = performance.now() - start; + +console.log( + JSON.stringify({ + function: "datetime_array", + mean_ms: total / ITERATIONS, + iterations: ITERATIONS, + total_ms: total, + }), +); diff --git a/benchmarks/tsb/bench_first_last_valid_index.ts b/benchmarks/tsb/bench_first_last_valid_index.ts new file mode 100644 index 00000000..33d04869 --- /dev/null +++ b/benchmarks/tsb/bench_first_last_valid_index.ts @@ -0,0 +1,52 @@ +/** + * Benchmark: firstValidIndex / lastValidIndex + * Outputs JSON: {"function": "first_last_valid_index", "mean_ms": ..., "iterations": ..., "total_ms": ...} + */ +import { Series, firstValidIndex, lastValidIndex } from "../../src/index.ts"; + +const N = 100_000; + +// Series where first valid is near the start (a few NaN/null at beginning) +const dataStart = Float64Array.from({ length: N }, (_, i) => + i < 10 ? NaN : i, +); +const seriesStart = new Series({ data: dataStart }); + +// Series where last valid is near the end (a few NaN/null at the end) +const dataEnd = Float64Array.from({ length: N }, (_, i) => + i >= N - 10 ? NaN : i, +); +const seriesEnd = new Series({ data: dataEnd }); + +// Series with NaN scattered throughout (worst-case scan) +const dataMixed = Float64Array.from({ length: N }, (_, i) => + i % 7 === 0 ? NaN : i, +); +const seriesMixed = new Series({ data: dataMixed }); + +// Warm-up +for (let w = 0; w < 20; w++) { + firstValidIndex(seriesStart); + lastValidIndex(seriesEnd); + firstValidIndex(seriesMixed); + lastValidIndex(seriesMixed); +} + +const iterations = 500; +const start = performance.now(); +for (let i = 0; i < iterations; i++) { + firstValidIndex(seriesStart); + lastValidIndex(seriesEnd); + firstValidIndex(seriesMixed); + lastValidIndex(seriesMixed); +} +const total_ms = performance.now() - start; + +console.log( + JSON.stringify({ + function: "first_last_valid_index", + mean_ms: total_ms / iterations, + iterations, + total_ms, + }), +); diff --git a/benchmarks/tsb/bench_gaussian_kde.ts b/benchmarks/tsb/bench_gaussian_kde.ts new file mode 100644 index 00000000..a0c639a5 --- /dev/null +++ b/benchmarks/tsb/bench_gaussian_kde.ts @@ -0,0 +1,40 @@ +/** + * Benchmark: Gaussian KDE on 10k data points — evaluate, pdf, integrate + */ +import { gaussianKDE } from "../../src/index.js"; + +const N = 10_000; +const EVAL_POINTS = 200; +const WARMUP = 3; +const ITERATIONS = 20; + +// Generate data from a bimodal distribution +const data: number[] = Array.from({ length: N }, (_, i) => { + const t = i / N; + return t < 0.5 ? Math.sin(i * 0.05) * 2 + 3 : Math.cos(i * 0.03) * 2 - 3; +}); + +const evalPoints: number[] = Array.from({ length: EVAL_POINTS }, (_, i) => -6 + i * 0.06); + +const kde = gaussianKDE(data); + +for (let i = 0; i < WARMUP; i++) { + kde.evaluate(evalPoints); + kde.integrate(-2, 2); +} + +const start = performance.now(); +for (let i = 0; i < ITERATIONS; i++) { + kde.evaluate(evalPoints); + kde.integrate(-2, 2); +} +const total = performance.now() - start; + +console.log( + JSON.stringify({ + function: "gaussian_kde", + mean_ms: total / ITERATIONS, + iterations: ITERATIONS, + total_ms: total, + }), +); diff --git a/benchmarks/tsb/bench_mode.ts b/benchmarks/tsb/bench_mode.ts new file mode 100644 index 00000000..c83d8326 --- /dev/null +++ b/benchmarks/tsb/bench_mode.ts @@ -0,0 +1,31 @@ +/** + * Benchmark: mode on 100k-element Series (mixed numeric with repeats) + */ +import { Series, modeSeries } from "../../src/index.js"; + +const ROWS = 100_000; +const WARMUP = 3; +const ITERATIONS = 10; + +// Create data with ~10 distinct values so mode is meaningful +const data = Float64Array.from({ length: ROWS }, (_, i) => i % 10); +const s = new Series(data); + +for (let i = 0; i < WARMUP; i++) { + modeSeries(s); +} + +const start = performance.now(); +for (let i = 0; i < ITERATIONS; i++) { + modeSeries(s); +} +const total = performance.now() - start; + +console.log( + JSON.stringify({ + function: "mode", + mean_ms: total / ITERATIONS, + iterations: ITERATIONS, + total_ms: total, + }), +); diff --git a/benchmarks/tsb/bench_string_array.ts b/benchmarks/tsb/bench_string_array.ts new file mode 100644 index 00000000..6902f524 --- /dev/null +++ b/benchmarks/tsb/bench_string_array.ts @@ -0,0 +1,40 @@ +/** + * Benchmark: StringArray — nullable string extension array operations. + * N=100_000 elements with ~10% nulls. Tests from/upper/lower/strip/contains/len/fillna. + */ +import { arrays } from "../../src/index.js"; + +const N = 100_000; +const WARMUP = 3; +const ITERATIONS = 50; + +const WORDS = ["hello", "world", " foo ", "bar", "baz", " qux ", "quux", "corge", "grault", "garply"]; + +const raw: (string | null)[] = Array.from({ length: N }, (_, i) => + i % 10 === 0 ? null : WORDS[i % WORDS.length], +); + +function run(): void { + const a = arrays.StringArray.from(raw); + a.upper(); + a.lower(); + a.strip(); + a.contains("oo"); + a.len(); + a.fillna("NA"); +} + +for (let i = 0; i < WARMUP; i++) run(); + +const start = performance.now(); +for (let i = 0; i < ITERATIONS; i++) run(); +const total = performance.now() - start; + +console.log( + JSON.stringify({ + function: "string_array", + mean_ms: total / ITERATIONS, + iterations: ITERATIONS, + total_ms: total, + }), +); diff --git a/benchmarks/tsb/bench_timedelta_array.ts b/benchmarks/tsb/bench_timedelta_array.ts new file mode 100644 index 00000000..b810adef --- /dev/null +++ b/benchmarks/tsb/bench_timedelta_array.ts @@ -0,0 +1,52 @@ +/** + * Benchmark: TimedeltaArray — create and operate on nullable timedelta arrays. + * Outputs JSON: {"function": "timedelta_array", "mean_ms": ..., "iterations": ..., "total_ms": ...} + */ +import { TimedeltaArray, Timedelta } from "../../src/index.js"; + +const SIZE = 100_000; +const WARMUP = 5; +const ITERATIONS = 50; + +// Build raw values: ~10% null +const values: (number | null)[] = Array.from({ length: SIZE }, (_, i) => + i % 10 === 0 ? null : i * 60_000, +); + +const fillValue = Timedelta.fromMilliseconds(0); + +function run(): void { + const arr = TimedeltaArray.from(values); + + // Component access + void arr.days; + void arr.hours; + void arr.totalSeconds; + + // Null checks + void arr.isna(); + void arr.notna(); + + // Aggregation + void arr.sum(); + void arr.min(); + void arr.max(); + + // Fill + void arr.fillna(fillValue); +} + +for (let i = 0; i < WARMUP; i++) run(); + +const start = performance.now(); +for (let i = 0; i < ITERATIONS; i++) run(); +const total = performance.now() - start; + +console.log( + JSON.stringify({ + function: "timedelta_array", + mean_ms: total / ITERATIONS, + iterations: ITERATIONS, + total_ms: total, + }), +);