diff --git a/benchmarks/pandas/bench_case_when.py b/benchmarks/pandas/bench_case_when.py new file mode 100644 index 00000000..6dfb8761 --- /dev/null +++ b/benchmarks/pandas/bench_case_when.py @@ -0,0 +1,35 @@ +"""Benchmark: case_when — conditional value selection on 100k-element Series (pandas 2.2+)""" +import json, time +import numpy as np +import pandas as pd + +ROWS = 100_000 +WARMUP = 5 +ITERATIONS = 20 + +data = np.arange(ROWS, dtype=float) % 100 +s = pd.Series(data) +cond1 = s < 25 +cond2 = s < 50 +cond3 = s < 75 + +caselist = [ + (cond1, "low"), + (cond2, "medium-low"), + (cond3, "medium-high"), +] + +for _ in range(WARMUP): + s.case_when(caselist) + +start = time.perf_counter() +for _ in range(ITERATIONS): + s.case_when(caselist) +total = (time.perf_counter() - start) * 1000 + +print(json.dumps({ + "function": "case_when", + "mean_ms": total / ITERATIONS, + "iterations": ITERATIONS, + "total_ms": total, +})) diff --git a/benchmarks/pandas/bench_contingency.py b/benchmarks/pandas/bench_contingency.py new file mode 100644 index 00000000..431f58cb --- /dev/null +++ b/benchmarks/pandas/bench_contingency.py @@ -0,0 +1,78 @@ +""" +Benchmark: contingency — expected_freq, relative_risk, odds_ratio, association +Pure-numpy equivalents (no scipy) matching the TypeScript benchmark. +Dataset: same 4x4 table as the TypeScript benchmark. +""" +import json +import time +import numpy as np + +WARMUP = 10 +ITERS = 50 + +observed = np.array([ + [120, 80, 40, 60], + [90, 110, 70, 30], + [50, 60, 100, 90], + [40, 50, 90, 120], +], dtype=float) + +two_by_two = np.array([ + [60.0, 40.0], + [30.0, 70.0], +]) + + +def expected_freq(obs): + row_sums = obs.sum(axis=1, keepdims=True) + col_sums = obs.sum(axis=0, keepdims=True) + total = obs.sum() + return row_sums * col_sums / total + + +def relative_risk(obs): + a, b = obs[0, 0], obs[0, 1] + c, d = obs[1, 0], obs[1, 1] + risk0 = a / (a + b) + risk1 = c / (c + d) + return risk0 / risk1 + + +def odds_ratio(obs): + a, b = obs[0, 0], obs[0, 1] + c, d = obs[1, 0], obs[1, 1] + return (a * d) / (b * c) + + +def association_cramer(obs): + exp = expected_freq(obs) + chi2 = np.sum((obs - exp) ** 2 / exp) + n = obs.sum() + r, c = obs.shape + return np.sqrt(chi2 / n / min(r - 1, c - 1)) + + +# Warm up +for _ in range(WARMUP): + expected_freq(observed) + relative_risk(two_by_two) + odds_ratio(two_by_two) + association_cramer(observed) + +# Measure +start = time.perf_counter() +for _ in range(ITERS): + expected_freq(observed) + relative_risk(two_by_two) + odds_ratio(two_by_two) + association_cramer(observed) +total_s = time.perf_counter() - start +total_ms = total_s * 1000 +mean_ms = total_ms / ITERS + +print(json.dumps({ + "function": "contingency", + "mean_ms": round(mean_ms, 4), + "iterations": ITERS, + "total_ms": round(total_ms, 4), +})) diff --git a/benchmarks/pandas/bench_entropy.py b/benchmarks/pandas/bench_entropy.py new file mode 100644 index 00000000..5c3f3f05 --- /dev/null +++ b/benchmarks/pandas/bench_entropy.py @@ -0,0 +1,44 @@ +import numpy as np +import json +import time + +N = 100 +WARMUP = 5 +ITERS = 50 + +p = np.arange(1, N + 1, dtype=float) +q = np.arange(N, 0, -1, dtype=float) + +# Normalise +p_norm = p / p.sum() +q_norm = q / q.sum() + + +def entropy_fn(pk): + pk = pk / pk.sum() + return -np.sum(pk * np.log(pk + 1e-300)) + + +def kl_divergence(pk, qk): + pk = pk / pk.sum() + qk = qk / qk.sum() + mask = pk > 0 + return np.sum(pk[mask] * np.log(pk[mask] / (qk[mask] + 1e-300))) + + +for _ in range(WARMUP): + entropy_fn(p) + kl_divergence(p, q) + +t0 = time.perf_counter() +for _ in range(ITERS): + entropy_fn(p) + kl_divergence(p, q) +total_ms = (time.perf_counter() - t0) * 1000 + +print(json.dumps({ + "function": "entropy_klDivergence", + "mean_ms": total_ms / ITERS, + "iterations": ITERS, + "total_ms": total_ms, +})) diff --git a/benchmarks/pandas/bench_feather.py b/benchmarks/pandas/bench_feather.py new file mode 100644 index 00000000..9a6424e8 --- /dev/null +++ b/benchmarks/pandas/bench_feather.py @@ -0,0 +1,42 @@ +""" +Benchmark: read_feather / to_feather — Arrow Feather v2 round-trip on 10k rows +""" +import json +import time +import io +import pandas as pd +import numpy as np + +ROWS = 10_000 +WARMUP = 3 +ITERATIONS = 20 + +rng = np.random.default_rng(42) +df = pd.DataFrame({ + "id": np.arange(ROWS, dtype=np.int64), + "value": np.arange(ROWS, dtype=np.float64) * 1.1, + "label": [f"cat_{i % 50}" for i in range(ROWS)], +}) + +# Warm up +for _ in range(WARMUP): + buf = io.BytesIO() + df.to_feather(buf) + buf.seek(0) + pd.read_feather(buf) + +# Measure round-trip +start = time.perf_counter() +for _ in range(ITERATIONS): + buf = io.BytesIO() + df.to_feather(buf) + buf.seek(0) + pd.read_feather(buf) +total = (time.perf_counter() - start) * 1000 + +print(json.dumps({ + "function": "feather", + "mean_ms": total / ITERATIONS, + "iterations": ITERATIONS, + "total_ms": total, +})) diff --git a/benchmarks/pandas/bench_flags_options.py b/benchmarks/pandas/bench_flags_options.py new file mode 100644 index 00000000..3b105d7d --- /dev/null +++ b/benchmarks/pandas/bench_flags_options.py @@ -0,0 +1,73 @@ +""" +Benchmark: flags and options (pandas equivalent) + +Measures: + - DataFrame.flags.allows_duplicate_labels get+set + - pd.get_option / pd.set_option / pd.reset_option for multiple keys + - pd.options proxy read + +Dataset: 10,000-row Series and DataFrame; 20 measured iterations. +""" + +import json +import time + +import numpy as np +import pandas as pd + +N = 10_000 +WARMUP = 5 +ITERS = 20 + +data = np.arange(N, dtype=np.float64) +s = pd.Series(data) +df = pd.DataFrame({"a": data, "b": data}) + + +def bench_flags_options(): + sink = 0 + for _ in range(ITERS + WARMUP): + # flags on Series + prev = s.flags.allows_duplicate_labels + s.flags.allows_duplicate_labels = not prev + s.flags.allows_duplicate_labels = prev + sink ^= int(s.flags.allows_duplicate_labels) + + # flags on DataFrame + prev_df = df.flags.allows_duplicate_labels + df.flags.allows_duplicate_labels = not prev_df + df.flags.allows_duplicate_labels = prev_df + sink ^= int(df.flags.allows_duplicate_labels) + + # options get/set/reset + v = pd.get_option("display.max_rows") + pd.set_option("display.max_rows", v + 1) + pd.reset_option("display.max_rows") + sink ^= int(pd.options.display.max_rows > 0) + + pd.set_option("display.max_columns", 20) + pd.reset_option("display.max_columns") + sink ^= int(pd.options.display.max_columns > 0) + + return sink + + +# Warm-up +bench_flags_options() + +# Measure +t0 = time.perf_counter() +for _ in range(ITERS): + bench_flags_options() +total = (time.perf_counter() - t0) * 1000 # ms + +print( + json.dumps( + { + "function": "flags_options", + "mean_ms": total / ITERS, + "iterations": ITERS, + "total_ms": total, + } + ) +) diff --git a/benchmarks/pandas/bench_floating_array.py b/benchmarks/pandas/bench_floating_array.py new file mode 100644 index 00000000..297062e2 --- /dev/null +++ b/benchmarks/pandas/bench_floating_array.py @@ -0,0 +1,42 @@ +""" +Benchmark: FloatingArray (pandas Float64 nullable float array). +N=100_000 elements with ~10% nulls. Tests from/sum/mean/min/max/add/fillna. +""" +import json +import time + +import pandas as pd +import numpy as np + +N = 100_000 +WARMUP = 3 +ITERATIONS = 20 + +raw = [None if i % 10 == 0 else (i % 1000) * 0.001 - 0.5 for i in range(N)] + +for _ in range(WARMUP): + a = pd.array(raw, dtype="Float64") + float(a.sum()) + float(a.mean()) + float(a.min()) + float(a.max()) + a + 1.0 + a.fillna(0.0) + +start = time.perf_counter() +for _ in range(ITERATIONS): + a = pd.array(raw, dtype="Float64") + float(a.sum()) + float(a.mean()) + float(a.min()) + float(a.max()) + a + 1.0 + a.fillna(0.0) +total = (time.perf_counter() - start) * 1000 + +print(json.dumps({ + "function": "floating_array", + "mean_ms": total / ITERATIONS, + "iterations": ITERATIONS, + "total_ms": total, +})) diff --git a/benchmarks/pandas/bench_gaussianKDE.py b/benchmarks/pandas/bench_gaussianKDE.py new file mode 100644 index 00000000..353cb996 --- /dev/null +++ b/benchmarks/pandas/bench_gaussianKDE.py @@ -0,0 +1,45 @@ +"""Benchmark: Gaussian KDE evaluate on 1000-point dataset at 200 grid points. +Uses Scott bandwidth (numpy equivalent of Silverman for univariate data). +Pure numpy implementation to match pages workflow constraints. +""" +import json +import time +import numpy as np + +N = 1_000 +GRID = 200 +WARMUP = 3 +ITERATIONS = 20 + +# Create dataset: mix of two gaussians (same as TS benchmark) +i_vals = np.arange(N) +data = np.sin(i_vals * 0.01) * 2 + np.where(i_vals % 2 == 0, 0.0, 5.0) + +xmin, xmax = -5.0, 10.0 +grid = np.linspace(xmin, xmax, GRID) + + +def gaussian_kde_evaluate(data: np.ndarray, points: np.ndarray) -> np.ndarray: + """Gaussian KDE with Silverman bandwidth, pure numpy.""" + n = len(data) + std = np.std(data, ddof=1) + bw = (4.0 / (3.0 * n)) ** 0.2 * std # Silverman rule + diff = points[:, np.newaxis] - data[np.newaxis, :] # (GRID, N) + kernels = np.exp(-0.5 * (diff / bw) ** 2) / (bw * np.sqrt(2 * np.pi)) + return kernels.mean(axis=1) + + +for _ in range(WARMUP): + gaussian_kde_evaluate(data, grid) + +start = time.perf_counter() +for _ in range(ITERATIONS): + gaussian_kde_evaluate(data, grid) +total = (time.perf_counter() - start) * 1000 + +print(json.dumps({ + "function": "gaussianKDE", + "mean_ms": total / ITERATIONS, + "iterations": ITERATIONS, + "total_ms": total, +})) diff --git a/benchmarks/pandas/bench_hypothesis_tests.py b/benchmarks/pandas/bench_hypothesis_tests.py new file mode 100644 index 00000000..ee6863b6 --- /dev/null +++ b/benchmarks/pandas/bench_hypothesis_tests.py @@ -0,0 +1,103 @@ +"""Benchmark: hypothesis_tests — scipy-style hypothesis tests vs pandas/scipy equivalents.""" +import json +import math +import time + +WARMUP = 3 +ITERS = 20 +N = 1000 + + +def make_data(n: int, seed: int) -> list: + arr = [] + x = seed + for _ in range(n): + x = (x * 1664525 + 1013904223) & 0xFFFFFFFF + arr.append((x & 0xFFFFFFFF) / 0x100000000) + return [v * 4 + 2 for v in arr] + + +a = make_data(N, 42) +b = make_data(N, 99) + +# Pure-numpy/stdlib implementations matching what tsb does +try: + import numpy as np + + def ttest1samp_np(x, popmean): + x = np.asarray(x) + n = len(x) + mean = x.mean() + se = x.std(ddof=1) / math.sqrt(n) + t = (mean - popmean) / se + return t + + def ttestind_np(x, y): + x, y = np.asarray(x), np.asarray(y) + nx, ny = len(x), len(y) + vx, vy = x.var(ddof=1), y.var(ddof=1) + se = math.sqrt(vx / nx + vy / ny) + t = (x.mean() - y.mean()) / se + return t + + def ttestrel_np(x, y): + d = np.asarray(x) - np.asarray(y) + n = len(d) + t = d.mean() / (d.std(ddof=1) / math.sqrt(n)) + return t + + def foneway_np(*groups): + grand = np.concatenate(groups) + grand_mean = grand.mean() + ss_between = sum(len(g) * (np.mean(g) - grand_mean) ** 2 for g in groups) + ss_within = sum(((np.asarray(g) - np.mean(g)) ** 2).sum() for g in groups) + df_between = len(groups) - 1 + df_within = len(grand) - len(groups) + F = (ss_between / df_between) / (ss_within / df_within) + return F + + def pearsonr_np(x, y): + x, y = np.asarray(x), np.asarray(y) + r = np.corrcoef(x, y)[0, 1] + return r + + def spearmanr_np(x, y): + x, y = np.asarray(x), np.asarray(y) + rx = np.argsort(np.argsort(x)).astype(float) + ry = np.argsort(np.argsort(y)).astype(float) + r = np.corrcoef(rx, ry)[0, 1] + return r + + def mannwhitneyu_np(x, y): + nx, ny = len(x), len(y) + all_vals = sorted([(v, 0) for v in x] + [(v, 1) for v in y], key=lambda t: t[0]) + ranks = list(range(1, nx + ny + 1)) + u1 = sum(ranks[i] for i, (_, g) in enumerate(all_vals) if g == 0) + u1 -= nx * (nx + 1) / 2 + return u1 + + HAS_NP = True +except ImportError: + HAS_NP = False + + +def bench(): + total = 0.0 + for i in range(WARMUP + ITERS): + t0 = time.perf_counter() + if HAS_NP: + ttest1samp_np(a, 2.5) + ttestind_np(a, b) + ttestrel_np(a, b) + foneway_np(a, b) + pearsonr_np(a, b) + spearmanr_np(a, b) + mannwhitneyu_np(a, b) + elapsed = (time.perf_counter() - t0) * 1000 + if i >= WARMUP: + total += elapsed + mean_ms = total / ITERS + print(json.dumps({"function": "hypothesis_tests", "mean_ms": mean_ms, "iterations": ITERS, "total_ms": total})) + + +bench() diff --git a/benchmarks/pandas/bench_integer_array.py b/benchmarks/pandas/bench_integer_array.py new file mode 100644 index 00000000..fb481445 --- /dev/null +++ b/benchmarks/pandas/bench_integer_array.py @@ -0,0 +1,41 @@ +"""Benchmark: IntegerArray — nullable integer extension array operations. +N=100_000 elements with ~10% nulls using pandas IntegerArray. +Tests: from_sequence, sum, mean, min, max, add scalar, fillna. +""" +import json +import time +import numpy as np +import pandas as pd + +N = 100_000 +WARMUP = 3 +ITERATIONS = 20 + +# Build input with ~10% nulls (same pattern as TS version) +raw = [(None if i % 10 == 0 else int((i % 1000) - 500)) for i in range(N)] + + +def run(): + a = pd.array(raw, dtype="Int32") + _ = a.sum(skipna=True) + _ = a.mean(skipna=True) + _ = a.min(skipna=True) + _ = a.max(skipna=True) + _ = a + 1 + _ = a.fillna(0) + + +for _ in range(WARMUP): + run() + +start = time.perf_counter() +for _ in range(ITERATIONS): + run() +total = (time.perf_counter() - start) * 1000 + +print(json.dumps({ + "function": "integer_array", + "mean_ms": total / ITERATIONS, + "iterations": ITERATIONS, + "total_ms": total, +})) diff --git a/benchmarks/pandas/bench_linregress_polyfit.py b/benchmarks/pandas/bench_linregress_polyfit.py new file mode 100644 index 00000000..c4c08a97 --- /dev/null +++ b/benchmarks/pandas/bench_linregress_polyfit.py @@ -0,0 +1,43 @@ +"""Benchmark: linregress and polyfit — simple linear regression and polynomial fit. +Dataset: 10,000 points, 20 iterations. +""" +import json +import time +import numpy as np + +N = 10_000 +WARMUP = 3 +ITERATIONS = 20 + +x = np.arange(N, dtype=float) / N +y = 2.5 * x + 1.0 + np.sin(np.arange(N) * 0.01) * 0.1 + + +def linregress_numpy(xs, ys): + """Simple OLS linear regression matching scipy.stats.linregress.""" + n = len(xs) + sx = xs.sum() + sy = ys.sum() + sxx = (xs * xs).sum() + sxy = (xs * ys).sum() + slope = (n * sxy - sx * sy) / (n * sxx - sx * sx) + intercept = (sy - slope * sx) / n + return slope, intercept + + +for _ in range(WARMUP): + linregress_numpy(x, y) + np.polyfit(x, y, 2) + +start = time.perf_counter() +for _ in range(ITERATIONS): + linregress_numpy(x, y) + np.polyfit(x, y, 2) +total = (time.perf_counter() - start) * 1000 + +print(json.dumps({ + "function": "linregress_polyfit", + "mean_ms": total / ITERATIONS, + "iterations": ITERATIONS, + "total_ms": total, +})) diff --git a/benchmarks/pandas/bench_lreshape.py b/benchmarks/pandas/bench_lreshape.py new file mode 100644 index 00000000..96b02a3e --- /dev/null +++ b/benchmarks/pandas/bench_lreshape.py @@ -0,0 +1,37 @@ +"""Benchmark: lreshape — wide-to-long reshape using named column groups. +Dataset: 10,000 rows with 4 value columns (v1..v4), 50 iterations. +""" +import json +import time +import numpy as np +import pandas as pd + +N = 10_000 +WARMUP = 3 +ITERATIONS = 50 + +ids = np.arange(N) +data = { + "id": ids, + "v1": ids * 1.0, + "v2": ids * 2.0, + "v3": ids * 3.0, + "v4": ids * 4.0, +} +df = pd.DataFrame(data) +groups = {"value": ["v1", "v2", "v3", "v4"]} + +for _ in range(WARMUP): + pd.lreshape(df, groups) + +start = time.perf_counter() +for _ in range(ITERATIONS): + pd.lreshape(df, groups) +total = (time.perf_counter() - start) * 1000 + +print(json.dumps({ + "function": "lreshape", + "mean_ms": total / ITERATIONS, + "iterations": ITERATIONS, + "total_ms": total, +})) diff --git a/benchmarks/pandas/bench_multivariate.py b/benchmarks/pandas/bench_multivariate.py new file mode 100644 index 00000000..4922a1d5 --- /dev/null +++ b/benchmarks/pandas/bench_multivariate.py @@ -0,0 +1,53 @@ +"""Benchmark: multivariate statistics — Mahalanobis distance, covariance matrix, PCA +Dataset: 500 observations x 5 features (matching the TypeScript benchmark) +""" +import json +import time +import numpy as np + +N = 500 +P = 5 +WARMUP = 3 +ITERATIONS = 20 + +# Generate deterministic dataset matching TS version +i_idx = np.arange(N) +j_idx = np.arange(P) +X = np.sin(i_idx[:, None] * 0.1 + j_idx[None, :]) * 10 + j_idx[None, :] * 2 # (N, P) + +u = X[0] +v = X[1] + +# Pre-compute covariance and diagonal inverse covariance +cov = np.cov(X, rowvar=False) # (P, P) sample covariance +diag_inv_cov = np.diag(1.0 / np.maximum(np.diag(cov), 1e-10)) # diagonal approx of VI + + +def run_iteration(X, u, v, diag_inv_cov): + # Mahalanobis distance with pre-computed VI + diff = u - v + dist = np.sqrt(diff @ diag_inv_cov @ diff) + # Covariance matrix + cov_m = np.cov(X, rowvar=False) + # PCA via SVD (3 components) + X_centered = X - X.mean(axis=0) + _, _, Vt = np.linalg.svd(X_centered, full_matrices=False) + components = Vt[:3] + scores = X_centered @ components.T + return dist, cov_m, scores + + +for _ in range(WARMUP): + run_iteration(X, u, v, diag_inv_cov) + +start = time.perf_counter() +for _ in range(ITERATIONS): + run_iteration(X, u, v, diag_inv_cov) +total = (time.perf_counter() - start) * 1000 + +print(json.dumps({ + "function": "multivariate", + "mean_ms": total / ITERATIONS, + "iterations": ITERATIONS, + "total_ms": total, +})) diff --git a/benchmarks/pandas/bench_mutual_information.py b/benchmarks/pandas/bench_mutual_information.py new file mode 100644 index 00000000..2d0bafe1 --- /dev/null +++ b/benchmarks/pandas/bench_mutual_information.py @@ -0,0 +1,68 @@ +import numpy as np +import json +import time + +N = 1000 +WARMUP = 5 +ITERS = 50 +CATS = 10 + +# Same paired observations as the TS benchmark +xs = np.array([i % CATS for i in range(N)]) +ys = np.array([(i % CATS) + (i // CATS) % 3 for i in range(N)]) + + +def mutual_information(xs, ys): + """Compute mutual information I(X;Y) from paired observations.""" + n = len(xs) + ux, cx = np.unique(xs, return_counts=True) + uy, cy = np.unique(ys, return_counts=True) + px = cx / n + py = cy / n + + # Joint counts + joint_counts = {} + for x, y in zip(xs, ys): + key = (int(x), int(y)) + joint_counts[key] = joint_counts.get(key, 0) + 1 + + mi = 0.0 + for (xi, yi), cnt in joint_counts.items(): + pxy = cnt / n + pxi = px[np.searchsorted(ux, xi)] + pyi = py[np.searchsorted(uy, yi)] + if pxy > 0: + mi += pxy * np.log(pxy / (pxi * pyi + 1e-300)) + return mi + + +def normalized_mi(xs, ys): + """Normalized mutual information (arithmetic normalization).""" + mi = mutual_information(xs, ys) + n = len(xs) + _, cx = np.unique(xs, return_counts=True) + _, cy = np.unique(ys, return_counts=True) + px = cx / n + py = cy / n + hx = -np.sum(px * np.log(px + 1e-300)) + hy = -np.sum(py * np.log(py + 1e-300)) + denom = (hx + hy) / 2 + return mi / denom if denom > 0 else 0.0 + + +for _ in range(WARMUP): + mutual_information(xs, ys) + normalized_mi(xs, ys) + +t0 = time.perf_counter() +for _ in range(ITERS): + mutual_information(xs, ys) + normalized_mi(xs, ys) +total_ms = (time.perf_counter() - t0) * 1000 + +print(json.dumps({ + "function": "mutual_information", + "mean_ms": total_ms / ITERS, + "iterations": ITERS, + "total_ms": total_ms, +})) diff --git a/benchmarks/pandas/bench_pipe_apply.py b/benchmarks/pandas/bench_pipe_apply.py new file mode 100644 index 00000000..0025b60d --- /dev/null +++ b/benchmarks/pandas/bench_pipe_apply.py @@ -0,0 +1,56 @@ +"""Benchmark: pipe / apply / applymap on 10,000-row datasets. + +Exercises three functional-pipeline operations: + - pipe: chain 4 transforms on a Series + - apply: element-wise function on 10k-element Series + - applymap: element-wise function on 10k x 3 DataFrame +""" +import json +import time +import numpy as np +import pandas as pd + +N = 10_000 +WARMUP = 5 +ITERATIONS = 20 + +raw = np.array([(i % 100) + 1 for i in range(N)], dtype=float) +series = pd.Series(raw, name="x") +df = pd.DataFrame({ + "a": [(i % 50) + 1 for i in range(N)], + "b": [(i % 30) + 1 for i in range(N)], + "c": [(i % 20) + 1 for i in range(N)], +}, dtype=float) + + +def run_pipe(s): + return s.pipe(lambda x: x + 1).pipe(lambda x: x * 2).pipe(lambda x: x - 1).pipe(lambda x: x / 2) + + +def apply_fn(v): + return v * 2 + 1 + + +def applymap_fn(v): + return v * 2 + + +# Warm-up +for _ in range(WARMUP): + run_pipe(series) + series.apply(apply_fn) + df.map(applymap_fn) + +start = time.perf_counter() +for _ in range(ITERATIONS): + run_pipe(series) + series.apply(apply_fn) + df.map(applymap_fn) +total = (time.perf_counter() - start) * 1000 + +print(json.dumps({ + "function": "pipe_apply", + "mean_ms": total / ITERATIONS, + "iterations": ITERATIONS, + "total_ms": total, +})) diff --git a/benchmarks/pandas/bench_readStata.py b/benchmarks/pandas/bench_readStata.py new file mode 100644 index 00000000..997a8762 --- /dev/null +++ b/benchmarks/pandas/bench_readStata.py @@ -0,0 +1,37 @@ +"""Benchmark: readStata / toStata round-trip on a 10k-row DataFrame""" +import json, time, tempfile, os +import numpy as np +import pandas as pd + +ROWS = 10_000 +WARMUP = 3 +ITERATIONS = 20 + +ids = np.arange(ROWS, dtype=np.int32) +values = np.sin(np.arange(ROWS) * 0.01) * 1000 +categories = np.array([f"cat_{i % 5}" for i in range(ROWS)]) + +df = pd.DataFrame({"id": ids, "value": values, "category": categories}) + +# Write to a temp Stata file so read_stata benchmarks read from disk +tmp = tempfile.NamedTemporaryFile(suffix=".dta", delete=False) +tmp.close() +df.to_stata(tmp.name, write_index=False) + +# Warm up +for _ in range(WARMUP): + pd.read_stata(tmp.name) + +start = time.perf_counter() +for _ in range(ITERATIONS): + pd.read_stata(tmp.name) +total = (time.perf_counter() - start) * 1000 + +os.unlink(tmp.name) + +print(json.dumps({ + "function": "readStata", + "mean_ms": total / ITERATIONS, + "iterations": ITERATIONS, + "total_ms": total, +})) diff --git a/benchmarks/pandas/bench_read_fwf.py b/benchmarks/pandas/bench_read_fwf.py new file mode 100644 index 00000000..b13760f6 --- /dev/null +++ b/benchmarks/pandas/bench_read_fwf.py @@ -0,0 +1,37 @@ +"""Benchmark: read_fwf — parse a fixed-width formatted text file into a DataFrame. +Dataset: 10,000 rows x 4 columns (id, name, value, flag). +""" +import json, time, io +import numpy as np +import pandas as pd + +ROWS = 10_000 +WARMUP = 3 +ITERATIONS = 10 + +# Build fixed-width text matching the TypeScript benchmark +lines = ["id name value flag"] +for i in range(ROWS): + id_col = str(i).rjust(6) + name_col = ("item" + str(i % 500)).ljust(10) + value_col = f"{np.sin(i * 0.01) * 1000:.3f}".rjust(10) + flag_col = ("Y" if i % 2 == 0 else "N").ljust(4) + lines.append(id_col + name_col + value_col + flag_col) +text = "\n".join(lines) + +colspecs = [(0, 6), (6, 16), (16, 26), (26, 30)] + +for _ in range(WARMUP): + pd.read_fwf(io.StringIO(text), colspecs=colspecs, header=0) + +start = time.perf_counter() +for _ in range(ITERATIONS): + pd.read_fwf(io.StringIO(text), colspecs=colspecs, header=0) +total = (time.perf_counter() - start) * 1000 + +print(json.dumps({ + "function": "readFwf", + "mean_ms": total / ITERATIONS, + "iterations": ITERATIONS, + "total_ms": total, +})) diff --git a/benchmarks/pandas/bench_read_sas.py b/benchmarks/pandas/bench_read_sas.py new file mode 100644 index 00000000..9af9c561 --- /dev/null +++ b/benchmarks/pandas/bench_read_sas.py @@ -0,0 +1,148 @@ +""" +Benchmark: pd.read_sas — parse a 1,000-row SAS XPORT (XPT) file. +Outputs JSON: {"function": "read_sas", "mean_ms": ..., "iterations": ..., "total_ms": ...} +""" +import json +import time +import io +import struct +import math +import numpy as np +import pandas as pd + +ROWS = 1_000 +WARMUP = 3 +ITERATIONS = 20 + +# ── IBM 370 double encoder ──────────────────────────────────────────────────── + +def ibm_encode(val: float) -> bytes: + if val == 0.0: + return b"\x00" * 8 + if not math.isfinite(val): + return b"\x2e" + b"\x00" * 7 + sign = 1 if val < 0 else 0 + abs_val = abs(val) + exp = 0 + mant = abs_val + while mant >= 1.0: + mant /= 16 + exp += 1 + while mant < 1.0 / 16 and mant > 0: + mant *= 16 + exp -= 1 + mant_int = round(mant * 2**56) + out = bytearray(8) + out[0] = (sign << 7) | ((exp + 64) & 0x7f) + for i in range(1, 8): + out[i] = (mant_int >> ((7 - i) * 8)) & 0xff + return bytes(out) + +# ── Minimal XPORT v5 builder ───────────────────────────────────────────────── + +def build_xpt(num_vars, char_vars, rows_data): + RECORD = 80 + + def pad80(s): + return s.ljust(RECORD).encode("ascii")[:RECORD] + + def write_u16(val): + return struct.pack(">H", val) + + def write_u32(val): + return struct.pack(">I", val) + + # Compute variable metadata + metas = [] + pos = 0 + for name in num_vars: + metas.append({"type": 1, "name": name, "len": 8, "pos": pos}) + pos += 8 + for name, length in char_vars: + metas.append({"type": 2, "name": name, "len": length, "pos": pos}) + pos += length + row_len = pos + + chunks = bytearray() + + # Library header (5 × 80 bytes) + chunks += pad80("HEADER RECORD*******LIBRARY HEADER RECORD!!!!!!!000000000000000000000000000000 ") + chunks += pad80("SAS SAS SASLIB 6.06 ASCII") + chunks += pad80("20240101") + chunks += pad80("") + chunks += pad80("") + + # Member header (3 × 80 bytes) + chunks += pad80("HEADER RECORD*******MEMBER HEADER RECORD!!!!!!!000000000000000000000000000001600000000140 ") + chunks += pad80("SAS BENCH SASDATA 6.06 ASCII") + chunks += pad80("") + + # Namestr header + nvar = len(metas) + chunks += pad80(f"HEADER RECORD*******NAMESTR HEADER RECORD!!!!!!!{nvar:06d}00000000000000000000 ") + + # Namestr records (140 bytes each) + ns_buf = bytearray(nvar * 140) + for i, m in enumerate(metas): + off = i * 140 + ns_buf[off:off+2] = write_u16(m["type"]) + ns_buf[off+2:off+4] = write_u16(140) + name_bytes = m["name"].encode("ascii").ljust(8)[:8] + ns_buf[off+4:off+12] = name_bytes + ns_buf[off+52:off+54] = write_u16(m["len"]) + ns_buf[off+84:off+88] = write_u32(m["pos"]) + padded_ns = math.ceil(len(ns_buf) / RECORD) * RECORD + ns_buf_padded = ns_buf.ljust(padded_ns, b"\x00") + chunks += ns_buf_padded + + # Obs header + chunks += pad80("HEADER RECORD*******OBS HEADER RECORD!!!!!!!000000000000000000000000000000 ") + + # Observations + padded_row_len = math.ceil(row_len / RECORD) * RECORD + obs_buf = bytearray(len(rows_data) * padded_row_len) + for r, row in enumerate(rows_data): + base = r * padded_row_len + for m in metas: + val = row.get(m["name"]) + if m["type"] == 1: + encoded = ibm_encode(float(val) if val is not None else 0.0) + obs_buf[base + m["pos"]:base + m["pos"] + 8] = encoded + else: + s = str(val) if val is not None else "" + b = s.encode("ascii")[:m["len"]].ljust(m["len"], b" ") + obs_buf[base + m["pos"]:base + m["pos"] + m["len"]] = b + + chunks += obs_buf + return bytes(chunks) + + +# ── Build dataset ───────────────────────────────────────────────────────────── + +rows_data = [ + {"id": float(i), "value": i * 1.5, "score": math.sin(i * 0.01), "label": f"item_{i % 100}"} + for i in range(ROWS) +] + +xpt_bytes = build_xpt( + ["id", "value", "score"], + [("label", 12)], + rows_data, +) + +# ── Benchmark ───────────────────────────────────────────────────────────────── + +for _ in range(WARMUP): + pd.read_sas(io.BytesIO(xpt_bytes), format="xport") + +start = time.perf_counter() +for _ in range(ITERATIONS): + pd.read_sas(io.BytesIO(xpt_bytes), format="xport") +total = (time.perf_counter() - start) * 1000 + +print(json.dumps({ + "function": "read_sas", + "mean_ms": total / ITERATIONS, + "iterations": ITERATIONS, + "total_ms": total, +})) diff --git a/benchmarks/pandas/bench_sql.py b/benchmarks/pandas/bench_sql.py new file mode 100644 index 00000000..1b419767 --- /dev/null +++ b/benchmarks/pandas/bench_sql.py @@ -0,0 +1,53 @@ +"""Benchmark: read_sql / to_sql on 10k-row DataFrames using SQLite in-memory""" +import json +import time +import math +import sqlite3 +import pandas as pd + +ROWS = 10_000 +WARMUP = 3 +ITERATIONS = 20 + +# ── Build a matching dataset ────────────────────────────────────────────────── +data = { + "id": list(range(ROWS)), + "value": [math.sin(i * 0.01) * 1000 for i in range(ROWS)], + "label": [f"item_{i % 100}" for i in range(ROWS)], +} +df = pd.DataFrame(data) + +# ── SQLite in-memory database ───────────────────────────────────────────────── +con = sqlite3.connect(":memory:") +df.to_sql("mock_table", con, index=False, if_exists="replace") + +# ── Warm-up reads ───────────────────────────────────────────────────────────── +for _ in range(WARMUP): + pd.read_sql_query("SELECT * FROM mock_table", con) + +# ── read_sql_query benchmark ────────────────────────────────────────────────── +start_read = time.perf_counter() +for _ in range(ITERATIONS): + pd.read_sql_query("SELECT * FROM mock_table", con) +total_read = (time.perf_counter() - start_read) * 1000 + +# ── Warm-up writes ──────────────────────────────────────────────────────────── +for _ in range(WARMUP): + df.to_sql("bench_table", con, index=False, if_exists="replace") + +# ── to_sql benchmark ────────────────────────────────────────────────────────── +start_write = time.perf_counter() +for _ in range(ITERATIONS): + df.to_sql("bench_table", con, index=False, if_exists="replace") +total_write = (time.perf_counter() - start_write) * 1000 + +con.close() + +print(json.dumps({ + "function": "sql", + "mean_ms": (total_read + total_write) / (2 * ITERATIONS), + "iterations": ITERATIONS, + "total_ms": total_read + total_write, + "read_mean_ms": total_read / ITERATIONS, + "write_mean_ms": total_write / ITERATIONS, +})) diff --git a/benchmarks/pandas/bench_to_excel.py b/benchmarks/pandas/bench_to_excel.py new file mode 100644 index 00000000..05b04472 --- /dev/null +++ b/benchmarks/pandas/bench_to_excel.py @@ -0,0 +1,27 @@ +"""Benchmark: to_excel — write a DataFrame to an XLSX buffer (BytesIO).""" +import json, time, io +import pandas as pd + +ROWS = 5_000 +WARMUP = 3 +ITERATIONS = 20 + +df = pd.DataFrame({ + "name": [f"name_{i % 1000}" for i in range(ROWS)], + "value": [i * 1.5 for i in range(ROWS)], + "flag": [i % 2 == 0 for i in range(ROWS)], +}) + +for _ in range(WARMUP): + buf = io.BytesIO() + df.to_excel(buf, index=True) + +times = [] +for _ in range(ITERATIONS): + buf = io.BytesIO() + t0 = time.perf_counter() + df.to_excel(buf, index=True) + times.append((time.perf_counter() - t0) * 1000) + +total_ms = sum(times) +print(json.dumps({"function": "to_excel", "mean_ms": round(total_ms / ITERATIONS, 3), "iterations": ITERATIONS, "total_ms": round(total_ms, 3)})) diff --git a/benchmarks/pandas/bench_xml.py b/benchmarks/pandas/bench_xml.py new file mode 100644 index 00000000..0c515f7b --- /dev/null +++ b/benchmarks/pandas/bench_xml.py @@ -0,0 +1,65 @@ +"""Benchmark: read_xml / to_xml — parse and serialize XML + +Creates a 1,000-row XML document, then benchmarks: + - pd.read_xml (parse XML string → DataFrame) + - df.to_xml (DataFrame → XML string) +""" +import json +import time +import io +import numpy as np +import pandas as pd + +ROWS = 1_000 +WARMUP = 3 +ITERATIONS = 20 + +# Build XML string with ROWS row elements (matching TS benchmark) +lines = ['', ""] +for i in range(ROWS): + lines.append( + f' ' + ) +lines.append("") +xml_string = "\n".join(lines) + +# Build a DataFrame for to_xml benchmarks +df = pd.DataFrame( + { + "id": np.arange(ROWS, dtype=np.int64), + "value": np.arange(ROWS, dtype=np.float64) * 1.1, + "label": [f"cat_{i % 50}" for i in range(ROWS)], + } +) + +# Warm up +for _ in range(WARMUP): + pd.read_xml(io.StringIO(xml_string)) + df.to_xml() + +# Benchmark read_xml +t0 = time.perf_counter() +for _ in range(ITERATIONS): + pd.read_xml(io.StringIO(xml_string)) +read_total = (time.perf_counter() - t0) * 1000 + +# Benchmark to_xml +t1 = time.perf_counter() +for _ in range(ITERATIONS): + df.to_xml() +write_total = (time.perf_counter() - t1) * 1000 + +total = read_total + write_total + +print( + json.dumps( + { + "function": "xml", + "mean_ms": total / (ITERATIONS * 2), + "iterations": ITERATIONS * 2, + "total_ms": total, + "read_mean_ms": read_total / ITERATIONS, + "write_mean_ms": write_total / ITERATIONS, + } + ) +) diff --git a/benchmarks/tsb/bench_case_when.ts b/benchmarks/tsb/bench_case_when.ts new file mode 100644 index 00000000..3435c0d2 --- /dev/null +++ b/benchmarks/tsb/bench_case_when.ts @@ -0,0 +1,39 @@ +/** + * Benchmark: caseWhen — conditional value selection on 100k-element Series + */ +import { Series, caseWhen } from "../../src/index.js"; + +const ROWS = 100_000; +const WARMUP = 5; +const ITERATIONS = 20; + +const data = Float64Array.from({ length: ROWS }, (_, i) => i % 100); +const s = new Series(data); +const cond1 = s.map((v) => (v as number) < 25); +const cond2 = s.map((v) => (v as number) < 50); +const cond3 = s.map((v) => (v as number) < 75); + +const caselist: [Series, string][] = [ + [cond1 as Series, "low"], + [cond2 as Series, "medium-low"], + [cond3 as Series, "medium-high"], +]; + +for (let i = 0; i < WARMUP; i++) { + caseWhen(s, caselist); +} + +const start = performance.now(); +for (let i = 0; i < ITERATIONS; i++) { + caseWhen(s, caselist); +} +const total = performance.now() - start; + +console.log( + JSON.stringify({ + function: "case_when", + mean_ms: total / ITERATIONS, + iterations: ITERATIONS, + total_ms: total, + }), +); diff --git a/benchmarks/tsb/bench_contingency.ts b/benchmarks/tsb/bench_contingency.ts new file mode 100644 index 00000000..609776c4 --- /dev/null +++ b/benchmarks/tsb/bench_contingency.ts @@ -0,0 +1,50 @@ +/** + * Benchmark: contingency — expectedFreq, relativeRisk, oddsRatio, association + * Dataset: 4×4 contingency table built from 100,000 categorised observations. + */ +import { expectedFreq, relativeRisk, oddsRatio, association } from "../../src/index.js"; + +const WARMUP = 10; +const ITERS = 50; + +// Build a 4×4 observed count table +const observed: readonly (readonly number[])[] = [ + [120, 80, 40, 60], + [90, 110, 70, 30], + [50, 60, 100, 90], + [40, 50, 90, 120], +]; + +// 2×2 table for relativeRisk / oddsRatio (requires exactly 2 rows × 2 cols) +const twoByTwo: readonly (readonly number[])[] = [ + [60, 40], + [30, 70], +]; + +// Warm up +for (let i = 0; i < WARMUP; i++) { + expectedFreq(observed); + relativeRisk(twoByTwo); + oddsRatio(twoByTwo); + association(observed, "cramer"); +} + +// Measure +const start = performance.now(); +for (let i = 0; i < ITERS; i++) { + expectedFreq(observed); + relativeRisk(twoByTwo); + oddsRatio(twoByTwo); + association(observed, "cramer"); +} +const total_ms = performance.now() - start; +const mean_ms = total_ms / ITERS; + +console.log( + JSON.stringify({ + function: "contingency", + mean_ms: parseFloat(mean_ms.toFixed(4)), + iterations: ITERS, + total_ms: parseFloat(total_ms.toFixed(4)), + }), +); diff --git a/benchmarks/tsb/bench_entropy.ts b/benchmarks/tsb/bench_entropy.ts new file mode 100644 index 00000000..f4783389 --- /dev/null +++ b/benchmarks/tsb/bench_entropy.ts @@ -0,0 +1,31 @@ +import { entropy, klDivergence } from "../../src/index.js"; + +const N = 100; +const WARMUP = 5; +const ITERS = 50; + +// Build two probability distributions of length N +const p: number[] = Array.from({ length: N }, (_, i) => i + 1); +const q: number[] = Array.from({ length: N }, (_, i) => N - i); + +let t0 = performance.now(); +for (let i = 0; i < WARMUP; i++) { + entropy(p); + klDivergence(p, q); +} +t0 = performance.now(); + +for (let i = 0; i < ITERS; i++) { + entropy(p); + klDivergence(p, q); +} +const total_ms = performance.now() - t0; + +console.log( + JSON.stringify({ + function: "entropy_klDivergence", + mean_ms: total_ms / ITERS, + iterations: ITERS, + total_ms, + }), +); diff --git a/benchmarks/tsb/bench_feather.ts b/benchmarks/tsb/bench_feather.ts new file mode 100644 index 00000000..b701fa1b --- /dev/null +++ b/benchmarks/tsb/bench_feather.ts @@ -0,0 +1,38 @@ +/** + * Benchmark: readFeather / toFeather — Arrow IPC Feather v2 round-trip on 10k rows + */ +import { DataFrame, toFeather, readFeather } from "../../src/index.js"; + +const ROWS = 10_000; +const WARMUP = 3; +const ITERATIONS = 20; + +// Build a DataFrame with int, float, and string columns +const ids = Array.from({ length: ROWS }, (_, i) => i); +const values = Array.from({ length: ROWS }, (_, i) => i * 1.1); +const labels = Array.from({ length: ROWS }, (_, i) => `cat_${i % 50}`); + +const df = new DataFrame({ id: ids, value: values, label: labels }); + +// Warm up +for (let i = 0; i < WARMUP; i++) { + const buf = toFeather(df); + readFeather(buf); +} + +// Measure round-trip +const start = performance.now(); +for (let i = 0; i < ITERATIONS; i++) { + const buf = toFeather(df); + readFeather(buf); +} +const total = performance.now() - start; + +console.log( + JSON.stringify({ + function: "feather", + mean_ms: total / ITERATIONS, + iterations: ITERATIONS, + total_ms: total, + }), +); diff --git a/benchmarks/tsb/bench_flags_options.ts b/benchmarks/tsb/bench_flags_options.ts new file mode 100644 index 00000000..d17204ce --- /dev/null +++ b/benchmarks/tsb/bench_flags_options.ts @@ -0,0 +1,76 @@ +/** + * Benchmark: flags and options + * + * Measures: + * - getFlags / allowsDuplicateLabels get+set (Series & DataFrame) + * - getOption / setOption / resetOption for multiple keys + * - options proxy read + * + * Dataset: 10,000-row Series and DataFrame; 20 measured iterations. + */ + +import { + Series, + DataFrame, + getFlags, + getOption, + setOption, + resetOption, + options, +} from "../../src/index.js"; + +const N = 10_000; +const WARMUP = 5; +const ITERS = 20; + +// Build test data once +const data = Float64Array.from({ length: N }, (_, i) => i); +const s = new Series(data); +const df = DataFrame.fromColumns({ a: Array.from(data), b: Array.from(data) }); + +function benchFlagsOptions(): number { + let sink = 0; + for (let i = 0; i < ITERS + WARMUP; i++) { + // flags on Series + const sf = getFlags(s); + const prev = sf.allowsDuplicateLabels; + sf.allowsDuplicateLabels = !prev; + sf.allowsDuplicateLabels = prev; + sink ^= sf.allowsDuplicateLabels ? 1 : 0; + + // flags on DataFrame + const dff = getFlags(df); + const prevDf = dff.allowsDuplicateLabels; + dff.allowsDuplicateLabels = !prevDf; + dff.allowsDuplicateLabels = prevDf; + sink ^= dff.allowsDuplicateLabels ? 1 : 0; + + // options get/set/reset + const v = getOption("display.max_rows") as number; + setOption("display.max_rows", v + 1); + resetOption("display.max_rows"); + sink ^= (options.display as Record).max_rows ? 1 : 0; + + setOption("display.max_columns", 20); + resetOption("display.max_columns"); + sink ^= (options.display as Record).max_columns ? 1 : 0; + } + return sink; +} + +// Warm-up +benchFlagsOptions(); + +// Measure +const t0 = performance.now(); +for (let i = 0; i < ITERS; i++) benchFlagsOptions(); +const total = performance.now() - t0; + +console.log( + JSON.stringify({ + function: "flags_options", + mean_ms: total / ITERS, + iterations: ITERS, + total_ms: total, + }), +); diff --git a/benchmarks/tsb/bench_floating_array.ts b/benchmarks/tsb/bench_floating_array.ts new file mode 100644 index 00000000..dedc4829 --- /dev/null +++ b/benchmarks/tsb/bench_floating_array.ts @@ -0,0 +1,45 @@ +/** + * Benchmark: FloatingArray — nullable float64 extension array operations. + * N=100_000 elements with ~10% nulls. Tests from/sum/mean/min/max/add/fillna. + */ +import { arrays } from "../../src/index.js"; + +const N = 100_000; +const WARMUP = 3; +const ITERATIONS = 20; + +// Build input with ~10% nulls +const raw: (number | null)[] = Array.from({ length: N }, (_, i) => + i % 10 === 0 ? null : (i % 1000) * 0.001 - 0.5, +); + +for (let i = 0; i < WARMUP; i++) { + const a = arrays.FloatingArray.from(raw, "Float64"); + a.sum(); + a.mean(); + a.min(); + a.max(); + a.add(1.0); + a.fillna(0.0); +} + +const start = performance.now(); +for (let i = 0; i < ITERATIONS; i++) { + const a = arrays.FloatingArray.from(raw, "Float64"); + a.sum(); + a.mean(); + a.min(); + a.max(); + a.add(1.0); + a.fillna(0.0); +} +const total = performance.now() - start; + +console.log( + JSON.stringify({ + function: "floating_array", + mean_ms: total / ITERATIONS, + iterations: ITERATIONS, + total_ms: total, + }), +); diff --git a/benchmarks/tsb/bench_gaussianKDE.ts b/benchmarks/tsb/bench_gaussianKDE.ts new file mode 100644 index 00000000..9e3f4de8 --- /dev/null +++ b/benchmarks/tsb/bench_gaussianKDE.ts @@ -0,0 +1,44 @@ +/** + * Benchmark: Gaussian KDE evaluate on 1000-point dataset at 200 grid points. + * Uses Silverman bandwidth (default). + */ +import { gaussianKDE } from "../../src/index.js"; + +const N = 1_000; +const GRID = 200; +const WARMUP = 3; +const ITERATIONS = 20; + +// Create dataset: mix of two gaussians +const data: number[] = []; +for (let i = 0; i < N; i++) { + const x = Math.sin(i * 0.01) * 2 + (i % 2 === 0 ? 0 : 5); + data.push(x); +} + +// Grid points to evaluate KDE at +const xmin = -5; +const xmax = 10; +const grid: number[] = Array.from({ length: GRID }, (_, i) => xmin + (i / (GRID - 1)) * (xmax - xmin)); + +// Warm-up +for (let i = 0; i < WARMUP; i++) { + const kde = gaussianKDE(data); + kde.evaluate(grid); +} + +const start = performance.now(); +for (let i = 0; i < ITERATIONS; i++) { + const kde = gaussianKDE(data); + kde.evaluate(grid); +} +const total = performance.now() - start; + +console.log( + JSON.stringify({ + function: "gaussianKDE", + mean_ms: total / ITERATIONS, + iterations: ITERATIONS, + total_ms: total, + }), +); diff --git a/benchmarks/tsb/bench_hypothesis_tests.ts b/benchmarks/tsb/bench_hypothesis_tests.ts new file mode 100644 index 00000000..05d36cb7 --- /dev/null +++ b/benchmarks/tsb/bench_hypothesis_tests.ts @@ -0,0 +1,41 @@ +import { ttest1samp, ttestInd, ttestRel, fOneway, pearsonr, spearmanr, mannWhitneyU } from "../../src/index.js"; + +const WARMUP = 3; +const ITERS = 20; +const N = 1000; + +// Seeded deterministic data +function makeData(n: number, seed: number): number[] { + const arr: number[] = []; + let x = seed; + for (let i = 0; i < n; i++) { + x = (x * 1664525 + 1013904223) & 0xffffffff; + arr.push((x >>> 0) / 0x100000000); + } + return arr; +} + +const a = makeData(N, 42).map((v) => v * 4 + 2); +const b = makeData(N, 99).map((v) => v * 4 + 2.5); + +function bench(): void { + let total = 0; + for (let i = 0; i < WARMUP + ITERS; i++) { + const t0 = performance.now(); + ttest1samp(a, 2.5); + ttestInd(a, b); + ttestRel(a, b); + fOneway([a, b]); + pearsonr(a, b); + spearmanr(a, b); + mannWhitneyU(a, b); + const elapsed = performance.now() - t0; + if (i >= WARMUP) total += elapsed; + } + const mean_ms = total / ITERS; + console.log( + JSON.stringify({ function: "hypothesis_tests", mean_ms, iterations: ITERS, total_ms: total }), + ); +} + +bench(); diff --git a/benchmarks/tsb/bench_integer_array.ts b/benchmarks/tsb/bench_integer_array.ts new file mode 100644 index 00000000..a2eaea3d --- /dev/null +++ b/benchmarks/tsb/bench_integer_array.ts @@ -0,0 +1,45 @@ +/** + * Benchmark: IntegerArray — nullable integer extension array operations. + * N=100_000 elements with ~10% nulls. Tests from/sum/mean/min/max/add/fillna. + */ +import { arrays } from "../../src/index.js"; + +const N = 100_000; +const WARMUP = 3; +const ITERATIONS = 20; + +// Build input with ~10% nulls +const raw: (number | null)[] = Array.from({ length: N }, (_, i) => + i % 10 === 0 ? null : (i % 1000) - 500, +); + +for (let i = 0; i < WARMUP; i++) { + const a = arrays.IntegerArray.from(raw, "Int32"); + a.sum(); + a.mean(); + a.min(); + a.max(); + a.add(1); + a.fillna(0); +} + +const start = performance.now(); +for (let i = 0; i < ITERATIONS; i++) { + const a = arrays.IntegerArray.from(raw, "Int32"); + a.sum(); + a.mean(); + a.min(); + a.max(); + a.add(1); + a.fillna(0); +} +const total = performance.now() - start; + +console.log( + JSON.stringify({ + function: "integer_array", + mean_ms: total / ITERATIONS, + iterations: ITERATIONS, + total_ms: total, + }), +); diff --git a/benchmarks/tsb/bench_linregress_polyfit.ts b/benchmarks/tsb/bench_linregress_polyfit.ts new file mode 100644 index 00000000..b1f100be --- /dev/null +++ b/benchmarks/tsb/bench_linregress_polyfit.ts @@ -0,0 +1,33 @@ +/** + * Benchmark: linregress and polyfit — simple linear regression and polynomial fit. + * Dataset: 10,000 points, 20 iterations. + */ +import { linregress, polyfit, polyval } from "../../src/index.js"; + +const N = 10_000; +const WARMUP = 3; +const ITERATIONS = 20; + +const x = Array.from({ length: N }, (_, i) => i / N); +const y = Array.from({ length: N }, (_, i) => 2.5 * (i / N) + 1.0 + Math.sin(i * 0.01) * 0.1); + +for (let i = 0; i < WARMUP; i++) { + linregress(x, y); + polyfit(x, y, 2); +} + +const start = performance.now(); +for (let i = 0; i < ITERATIONS; i++) { + linregress(x, y); + polyfit(x, y, 2); +} +const total = performance.now() - start; + +console.log( + JSON.stringify({ + function: "linregress_polyfit", + mean_ms: total / ITERATIONS, + iterations: ITERATIONS, + total_ms: total, + }), +); diff --git a/benchmarks/tsb/bench_lreshape.ts b/benchmarks/tsb/bench_lreshape.ts new file mode 100644 index 00000000..58f94459 --- /dev/null +++ b/benchmarks/tsb/bench_lreshape.ts @@ -0,0 +1,37 @@ +/** + * Benchmark: lreshape — wide-to-long reshape using named column groups. + * Dataset: 10,000 rows with 4 value columns (v1..v4), 50 iterations. + */ +import { DataFrame, lreshape } from "../../src/index.js"; + +const N = 10_000; +const WARMUP = 3; +const ITERATIONS = 50; + +const ids = Array.from({ length: N }, (_, i) => i); +const v1 = Array.from({ length: N }, (_, i) => i * 1.0); +const v2 = Array.from({ length: N }, (_, i) => i * 2.0); +const v3 = Array.from({ length: N }, (_, i) => i * 3.0); +const v4 = Array.from({ length: N }, (_, i) => i * 4.0); + +const df = DataFrame.fromColumns({ id: ids, v1, v2, v3, v4 }); +const groups = { value: ["v1", "v2", "v3", "v4"] }; + +for (let i = 0; i < WARMUP; i++) { + lreshape(df, groups); +} + +const start = performance.now(); +for (let i = 0; i < ITERATIONS; i++) { + lreshape(df, groups); +} +const total = performance.now() - start; + +console.log( + JSON.stringify({ + function: "lreshape", + mean_ms: total / ITERATIONS, + iterations: ITERATIONS, + total_ms: total, + }), +); diff --git a/benchmarks/tsb/bench_multivariate.ts b/benchmarks/tsb/bench_multivariate.ts new file mode 100644 index 00000000..199ec9e7 --- /dev/null +++ b/benchmarks/tsb/bench_multivariate.ts @@ -0,0 +1,49 @@ +/** + * Benchmark: multivariate statistics — mahalanobis distance, covMatrix, PCA + * Dataset: 500 observations × 5 features (realistic small-to-medium size) + */ +import { mahalanobis, covMatrix, PCA } from "../../src/index.js"; + +const N = 500; +const P = 5; +const WARMUP = 3; +const ITERATIONS = 20; + +// Generate a deterministic dataset +const X: number[][] = Array.from({ length: N }, (_, i) => + Array.from({ length: P }, (_, j) => Math.sin(i * 0.1 + j) * 10 + j * 2), +); + +const u = X[0]!; +const v = X[1]!; + +// Pre-compute inverse covariance for mahalanobis +const cov = covMatrix(X); +// Simple diagonal approximation for VI (invertMatrix is tested via mahalanobis internals) +const VI: number[][] = Array.from({ length: P }, (_, i) => + Array.from({ length: P }, (_, j) => (i === j ? 1 / Math.max(cov[i]![i]!, 1e-10) : 0)), +); + +// Warm up +for (let i = 0; i < WARMUP; i++) { + mahalanobis(u, v, VI); + covMatrix(X); + new PCA({ n_components: 3 }).fit(X); +} + +const start = performance.now(); +for (let i = 0; i < ITERATIONS; i++) { + mahalanobis(u, v, VI); + covMatrix(X); + new PCA({ n_components: 3 }).fit(X); +} +const total = performance.now() - start; + +console.log( + JSON.stringify({ + function: "multivariate", + mean_ms: total / ITERATIONS, + iterations: ITERATIONS, + total_ms: total, + }), +); diff --git a/benchmarks/tsb/bench_mutual_information.ts b/benchmarks/tsb/bench_mutual_information.ts new file mode 100644 index 00000000..0f8df611 --- /dev/null +++ b/benchmarks/tsb/bench_mutual_information.ts @@ -0,0 +1,34 @@ +import { mutualInformation, normalizedMI } from "../../src/index.js"; + +const N = 1000; +const WARMUP = 5; +const ITERS = 50; + +// Build paired observations: two correlated categorical variables (10 categories each) +const CATS = 10; +const pairs: [number, number][] = Array.from({ length: N }, (_, i) => [ + i % CATS, + (i % CATS) + Math.floor(i / CATS) % 3, +]); + +let t0 = performance.now(); +for (let i = 0; i < WARMUP; i++) { + mutualInformation(pairs); + normalizedMI(pairs, "arithmetic"); +} +t0 = performance.now(); + +for (let i = 0; i < ITERS; i++) { + mutualInformation(pairs); + normalizedMI(pairs, "arithmetic"); +} +const total_ms = performance.now() - t0; + +console.log( + JSON.stringify({ + function: "mutual_information", + mean_ms: total_ms / ITERS, + iterations: ITERS, + total_ms, + }), +); diff --git a/benchmarks/tsb/bench_pipe_apply.ts b/benchmarks/tsb/bench_pipe_apply.ts new file mode 100644 index 00000000..29213f06 --- /dev/null +++ b/benchmarks/tsb/bench_pipe_apply.ts @@ -0,0 +1,45 @@ +/** + * Benchmark: pipe / seriesApply / dataFrameApplyMap on 10,000-row datasets. + * + * Exercises three functional-pipeline operations: + * - pipe: chain 4 transforms on a Series + * - seriesApply: element-wise function on 10k-element Series + * - dataFrameApplyMap: element-wise function on 10k × 3 DataFrame + */ +import { Series, DataFrame, pipe, seriesApply, dataFrameApplyMap } from "../../src/index.js"; + +const N = 10_000; +const WARMUP = 5; +const ITERATIONS = 20; + +const raw = Float64Array.from({ length: N }, (_, i) => (i % 100) + 1); +const series = new Series(raw, { name: "x" }); +const df = DataFrame.fromColumns({ + a: Array.from({ length: N }, (_, i) => (i % 50) + 1), + b: Array.from({ length: N }, (_, i) => (i % 30) + 1), + c: Array.from({ length: N }, (_, i) => (i % 20) + 1), +}); + +// Warm-up +for (let i = 0; i < WARMUP; i++) { + pipe(series, (s) => s.add(1), (s) => s.mul(2), (s) => s.sub(1), (s) => s.div(2)); + seriesApply(series, (v) => (v as number) * 2 + 1); + dataFrameApplyMap(df, (v) => (v as number) * 2); +} + +const start = performance.now(); +for (let i = 0; i < ITERATIONS; i++) { + pipe(series, (s) => s.add(1), (s) => s.mul(2), (s) => s.sub(1), (s) => s.div(2)); + seriesApply(series, (v) => (v as number) * 2 + 1); + dataFrameApplyMap(df, (v) => (v as number) * 2); +} +const total = performance.now() - start; + +console.log( + JSON.stringify({ + function: "pipe_apply", + mean_ms: total / ITERATIONS, + iterations: ITERATIONS, + total_ms: total, + }), +); diff --git a/benchmarks/tsb/bench_readStata.ts b/benchmarks/tsb/bench_readStata.ts new file mode 100644 index 00000000..a6703324 --- /dev/null +++ b/benchmarks/tsb/bench_readStata.ts @@ -0,0 +1,42 @@ +/** + * Benchmark: readStata / toStata round-trip on a 10k-row DataFrame + */ +import { DataFrame, Series, readStata, toStata } from "../../src/index.js"; + +const ROWS = 10_000; +const WARMUP = 3; +const ITERATIONS = 20; + +// Build a DataFrame with numeric and string columns +const ids = Int32Array.from({ length: ROWS }, (_, i) => i); +const values = Float64Array.from({ length: ROWS }, (_, i) => Math.sin(i * 0.01) * 1000); +const categories = Array.from({ length: ROWS }, (_, i) => `cat_${i % 5}`); + +const df = new DataFrame({ + id: new Series(ids), + value: new Series(values), + category: new Series(categories), +}); + +// Serialize once so readStata benchmarks read from a pre-built buffer +const buf = toStata(df); + +// Warm up +for (let i = 0; i < WARMUP; i++) { + readStata(buf); +} + +const start = performance.now(); +for (let i = 0; i < ITERATIONS; i++) { + readStata(buf); +} +const total = performance.now() - start; + +console.log( + JSON.stringify({ + function: "readStata", + mean_ms: total / ITERATIONS, + iterations: ITERATIONS, + total_ms: total, + }), +); diff --git a/benchmarks/tsb/bench_read_fwf.ts b/benchmarks/tsb/bench_read_fwf.ts new file mode 100644 index 00000000..f6d7eace --- /dev/null +++ b/benchmarks/tsb/bench_read_fwf.ts @@ -0,0 +1,39 @@ +/** + * Benchmark: readFwf — parse a fixed-width formatted text file into a DataFrame. + * Dataset: 10,000 rows × 4 columns (id, name, value, flag). + */ +import { readFwf } from "../../src/index.js"; + +const ROWS = 10_000; +const WARMUP = 3; +const ITERATIONS = 10; + +// Build a fixed-width text: id(6), name(10), value(10), flag(4) +const lines: string[] = ["id name value flag"]; +for (let i = 0; i < ROWS; i++) { + const id = String(i).padStart(6); + const name = ("item" + (i % 500)).padEnd(10); + const value = (Math.sin(i * 0.01) * 1000).toFixed(3).padStart(10); + const flag = (i % 2 === 0 ? "Y" : "N").padEnd(4); + lines.push(id + name + value + flag); +} +const text = lines.join("\n"); + +for (let i = 0; i < WARMUP; i++) { + readFwf(text, { colspecs: [[0, 6], [6, 16], [16, 26], [26, 30]] }); +} + +const start = performance.now(); +for (let i = 0; i < ITERATIONS; i++) { + readFwf(text, { colspecs: [[0, 6], [6, 16], [16, 26], [26, 30]] }); +} +const total = performance.now() - start; + +console.log( + JSON.stringify({ + function: "readFwf", + mean_ms: total / ITERATIONS, + iterations: ITERATIONS, + total_ms: total, + }), +); diff --git a/benchmarks/tsb/bench_read_sas.ts b/benchmarks/tsb/bench_read_sas.ts new file mode 100644 index 00000000..dd873ef7 --- /dev/null +++ b/benchmarks/tsb/bench_read_sas.ts @@ -0,0 +1,148 @@ +/** + * Benchmark: readSas — parse a 1,000-row SAS XPORT (XPT) file. + * Outputs JSON: {"function": "read_sas", "mean_ms": ..., "iterations": ..., "total_ms": ...} + */ +import { readSas } from "../../src/index.ts"; + +// ─── IBM 370 floating-point encoder ────────────────────────────────────────── + +function ibmEncode(val: number): Uint8Array { + const out = new Uint8Array(8); + if (val === 0) return out; + if (!Number.isFinite(val)) { out[0] = 0x2e; return out; } + const sign = val < 0 ? 1 : 0; + const abs = Math.abs(val); + let exp = 0; + let mant = abs; + while (mant >= 1) { mant /= 16; exp++; } + while (mant < 1 / 16 && mant > 0) { mant *= 16; exp--; } + const mantInt = BigInt(Math.round(mant * 2 ** 56)); + out[0] = (sign << 7) | ((exp + 64) & 0x7f); + for (let i = 1; i <= 7; i++) { + out[i] = Number((mantInt >> BigInt((7 - i) * 8)) & 0xffn); + } + return out; +} + +// ─── Minimal XPORT v5 builder ──────────────────────────────────────────────── + +function buildXpt( + numVars: readonly string[], + charVars: readonly { name: string; len: number }[], + rows: readonly Readonly>[], +): Uint8Array { + const RECORD = 80; + + function encodeAscii(s: string, maxLen: number): Uint8Array { + const buf = new Uint8Array(maxLen); + for (let i = 0; i < Math.min(s.length, maxLen); i++) buf[i] = s.charCodeAt(i) & 0x7f; + return buf; + } + function padTo80(s: string): Uint8Array { return encodeAscii(s.padEnd(RECORD, " "), RECORD); } + function writeU16(b: Uint8Array, o: number, v: number) { b[o] = (v >> 8) & 0xff; b[o + 1] = v & 0xff; } + function writeU32(b: Uint8Array, o: number, v: number) { + b[o] = (v >> 24) & 0xff; b[o + 1] = (v >> 16) & 0xff; b[o + 2] = (v >> 8) & 0xff; b[o + 3] = v & 0xff; + } + + type Meta = { type: 1 | 2; name: string; len: number; pos: number }; + const metas: Meta[] = []; + let pos = 0; + for (const name of numVars) { metas.push({ type: 1, name, len: 8, pos }); pos += 8; } + for (const { name, len } of charVars) { metas.push({ type: 2, name, len, pos }); pos += len; } + const rowLen = pos; + + const chunks: Uint8Array[] = []; + + // Library header (5 × 80 bytes) + chunks.push(padTo80("HEADER RECORD*******LIBRARY HEADER RECORD!!!!!!!000000000000000000000000000000 ")); + chunks.push(padTo80("SAS SAS SASLIB 6.06 ASCII")); + chunks.push(padTo80("20240101")); + chunks.push(padTo80("")); + chunks.push(padTo80("")); + + // Member header (3 × 80 bytes) + chunks.push(padTo80("HEADER RECORD*******MEMBER HEADER RECORD!!!!!!!000000000000000000000000000001600000000140 ")); + chunks.push(padTo80("SAS BENCH SASDATA 6.06 ASCII")); + chunks.push(padTo80("")); + + // Namestr header + const nvar = metas.length; + chunks.push(padTo80(`HEADER RECORD*******NAMESTR HEADER RECORD!!!!!!!${String(nvar).padStart(6, "0")}00000000000000000000 `)); + + // Namestr records (140 bytes each) + const nsBuf = new Uint8Array(nvar * 140); + for (let i = 0; i < metas.length; i++) { + const m = metas[i]!; + const off = i * 140; + writeU16(nsBuf, off, m.type); + writeU16(nsBuf, off + 2, 140); + nsBuf.set(encodeAscii(m.name, 8), off + 4); + writeU16(nsBuf, off + 52, m.len); + writeU32(nsBuf, off + 84, m.pos); + } + const nsPadded = Math.ceil(nsBuf.length / RECORD) * RECORD; + const nsPaddedBuf = new Uint8Array(nsPadded); + nsPaddedBuf.set(nsBuf); + chunks.push(nsPaddedBuf); + + // Obs header + chunks.push(padTo80("HEADER RECORD*******OBS HEADER RECORD!!!!!!!000000000000000000000000000000 ")); + + // Observations + const paddedRowLen = Math.ceil(rowLen / RECORD) * RECORD; + const obsBuf = new Uint8Array(rows.length * paddedRowLen); + for (let r = 0; r < rows.length; r++) { + const base = r * paddedRowLen; + const row = rows[r]!; + for (const m of metas) { + const val = row[m.name]; + if (m.type === 1) { + const encoded = ibmEncode(typeof val === "number" ? val : 0); + obsBuf.set(encoded, base + m.pos); + } else { + const s = typeof val === "string" ? val : ""; + obsBuf.set(encodeAscii(s, m.len), base + m.pos); + } + } + } + chunks.push(obsBuf); + + let total = 0; + for (const c of chunks) total += c.length; + const out = new Uint8Array(total); + let off = 0; + for (const c of chunks) { out.set(c, off); off += c.length; } + return out; +} + +// ─── Build dataset ──────────────────────────────────────────────────────────── + +const ROWS = 1_000; +const WARMUP = 3; +const ITERATIONS = 20; + +const rows: Readonly>[] = Array.from({ length: ROWS }, (_, i) => ({ + id: i, + value: i * 1.5, + score: Math.sin(i * 0.01), + label: `item_${i % 100}`, +})); + +const xpt = buildXpt(["id", "value", "score"], [{ name: "label", len: 12 }], rows); + +// ─── Benchmark ──────────────────────────────────────────────────────────────── + +for (let i = 0; i < WARMUP; i++) readSas(xpt); + +const start = performance.now(); +for (let i = 0; i < ITERATIONS; i++) readSas(xpt); +const total = performance.now() - start; + +console.log( + JSON.stringify({ + function: "read_sas", + mean_ms: total / ITERATIONS, + iterations: ITERATIONS, + total_ms: total, + }), +); diff --git a/benchmarks/tsb/bench_sql.ts b/benchmarks/tsb/bench_sql.ts new file mode 100644 index 00000000..5889d38c --- /dev/null +++ b/benchmarks/tsb/bench_sql.ts @@ -0,0 +1,85 @@ +/** + * Benchmark: readSqlQuery / toSql on 10k-row result sets + */ +import { DataFrame, readSqlQuery, toSql } from "../../src/index.js"; +import type { SqlConnection, SqlResult, SqlRow, SqlValue, IfExistsStrategy } from "../../src/index.js"; + +const ROWS = 10_000; +const WARMUP = 3; +const ITERATIONS = 20; + +// ── Shared result set ───────────────────────────────────────────────────────── +const columns: string[] = ["id", "value", "label"]; +const rows: SqlRow[] = Array.from({ length: ROWS }, (_, i) => ({ + id: i, + value: Math.sin(i * 0.01) * 1000, + label: `item_${i % 100}`, +})); + +// ── Mock adapter for reads ──────────────────────────────────────────────────── +class ReadAdapter implements SqlConnection { + query(_sql: string, _params?: readonly SqlValue[]): SqlResult { + return { columns, rows }; + } + listTables(): readonly string[] { + return ["mock_table"]; + } +} + +const readConn = new ReadAdapter(); + +// ── Warm-up reads ───────────────────────────────────────────────────────────── +for (let i = 0; i < WARMUP; i++) { + readSqlQuery("SELECT * FROM mock_table", readConn); +} + +// ── readSqlQuery benchmark ──────────────────────────────────────────────────── +const startRead = performance.now(); +for (let i = 0; i < ITERATIONS; i++) { + readSqlQuery("SELECT * FROM mock_table", readConn); +} +const totalRead = performance.now() - startRead; + +// ── Mock adapter for writes ─────────────────────────────────────────────────── +class WriteAdapter implements SqlConnection { + query(_sql: string, _params?: readonly SqlValue[]): SqlResult { + return { columns: [], rows: [] }; + } + listTables(): readonly string[] { + return []; + } + insert( + _tableName: string, + _rows: readonly SqlRow[], + _columns: readonly string[], + _ifExists: IfExistsStrategy, + ): number { + return _rows.length; + } +} + +const writeConn = new WriteAdapter(); +const df = readSqlQuery("SELECT * FROM mock_table", readConn); + +// ── Warm-up writes ──────────────────────────────────────────────────────────── +for (let i = 0; i < WARMUP; i++) { + toSql(df, "bench_table", writeConn, { ifExists: "replace" }); +} + +// ── toSql benchmark ─────────────────────────────────────────────────────────── +const startWrite = performance.now(); +for (let i = 0; i < ITERATIONS; i++) { + toSql(df, "bench_table", writeConn, { ifExists: "replace" }); +} +const totalWrite = performance.now() - startWrite; + +console.log( + JSON.stringify({ + function: "sql", + mean_ms: (totalRead + totalWrite) / (2 * ITERATIONS), + iterations: ITERATIONS, + total_ms: totalRead + totalWrite, + read_mean_ms: totalRead / ITERATIONS, + write_mean_ms: totalWrite / ITERATIONS, + }), +); diff --git a/benchmarks/tsb/bench_to_excel.ts b/benchmarks/tsb/bench_to_excel.ts new file mode 100644 index 00000000..941535c2 --- /dev/null +++ b/benchmarks/tsb/bench_to_excel.ts @@ -0,0 +1,38 @@ +/** + * Benchmark: toExcel — serialize a DataFrame to an XLSX binary buffer. + * Outputs JSON: {"function": "to_excel", "mean_ms": ..., "iterations": ..., "total_ms": ...} + */ +import { DataFrame, toExcel } from "../../src/index.ts"; + +const ROWS = 5_000; +const WARMUP = 3; +const ITERATIONS = 20; + +const index = Array.from({ length: ROWS }, (_, i) => i); +const colA = Array.from({ length: ROWS }, (_, i) => `name_${i % 1000}`); +const colB = Array.from({ length: ROWS }, (_, i) => i * 1.5); +const colC = Array.from({ length: ROWS }, (_, i) => i % 2 === 0); + +const df = new DataFrame({ name: colA, value: colB, flag: colC }, { index }); + +for (let i = 0; i < WARMUP; i++) { + toExcel(df); +} + +const times: number[] = []; +for (let i = 0; i < ITERATIONS; i++) { + const start = performance.now(); + toExcel(df); + times.push(performance.now() - start); +} + +const totalMs = times.reduce((a, b) => a + b, 0); +const meanMs = totalMs / ITERATIONS; +console.log( + JSON.stringify({ + function: "to_excel", + mean_ms: Math.round(meanMs * 1000) / 1000, + iterations: ITERATIONS, + total_ms: Math.round(totalMs * 1000) / 1000, + }), +); diff --git a/benchmarks/tsb/bench_xml.ts b/benchmarks/tsb/bench_xml.ts new file mode 100644 index 00000000..a10f1eb6 --- /dev/null +++ b/benchmarks/tsb/bench_xml.ts @@ -0,0 +1,65 @@ +/** + * Benchmark: readXml / toXml — parse and serialize XML + * + * Creates a 1,000-row XML document, then benchmarks: + * - readXml (parse XML string → DataFrame) + * - toXml (DataFrame → XML string) + */ +import { readXml, toXml, DataFrame, Series } from "../../src/index.js"; + +const ROWS = 1_000; +const WARMUP = 3; +const ITERATIONS = 20; + +// Build XML string with ROWS row elements +const lines: string[] = ['', ""]; +for (let i = 0; i < ROWS; i++) { + lines.push( + ` `, + ); +} +lines.push(""); +const xmlString = lines.join("\n"); + +// Build a DataFrame for toXml benchmarks +const ids = Array.from({ length: ROWS }, (_, i) => i); +const values = Array.from({ length: ROWS }, (_, i) => i * 1.1); +const labels = Array.from({ length: ROWS }, (_, i) => `cat_${i % 50}`); +const df = new DataFrame({ + id: new Series(ids), + value: new Series(values), + label: new Series(labels), +}); + +// Warm up +for (let i = 0; i < WARMUP; i++) { + readXml(xmlString); + toXml(df); +} + +// Benchmark readXml +const t0 = performance.now(); +for (let i = 0; i < ITERATIONS; i++) { + readXml(xmlString); +} +const readTotal = performance.now() - t0; + +// Benchmark toXml +const t1 = performance.now(); +for (let i = 0; i < ITERATIONS; i++) { + toXml(df); +} +const writeTotal = performance.now() - t1; + +const total = readTotal + writeTotal; + +console.log( + JSON.stringify({ + function: "xml", + mean_ms: total / (ITERATIONS * 2), + iterations: ITERATIONS * 2, + total_ms: total, + read_mean_ms: readTotal / ITERATIONS, + write_mean_ms: writeTotal / ITERATIONS, + }), +);