Skip to content

Commit 36dd643

Browse files
Iteration 247: 5 new benchmark pairs (539 total, +5 vs best 534)
Add 5 new benchmark pairs: - bench_formatter_factories: makeFloatFormatter/makePercentFormatter/makeCurrencyFormatter applied to Series - bench_cat_intersect_diff: catIntersectCategories/catDiffCategories on 100k-row Series with 20 categories - bench_series_shift_fn: standalone shiftSeries (vs old manual-impl bench_series_shift) - bench_reindex_fill: reindexSeries with ffill/bfill fill methods (extending bench_reindex) - bench_sample_weighted: sampleSeries with probability weights (extending bench_sample_fn) Run: https://github.com/githubnext/tsessebe/actions/runs/24650741846 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 5291ffd commit 36dd643

10 files changed

Lines changed: 423 additions & 0 deletions
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
"""
2+
Benchmark: pandas category set operations — intersection and difference of
3+
categorical Series categories (100k-element, 20 categories each).
4+
Mirrors tsb's catIntersectCategories / catDiffCategories.
5+
Outputs JSON: {"function": "cat_intersect_diff", "mean_ms": ..., "iterations": ..., "total_ms": ...}
6+
"""
7+
import json
8+
import time
9+
import pandas as pd
10+
11+
SIZE = 100_000
12+
WARMUP = 5
13+
ITERATIONS = 30
14+
15+
cats_a = [f"cat_a_{i}" for i in range(20)]
16+
cats_b = [f"cat_{'a' if i < 10 else 'b'}_{i}" for i in range(20)]
17+
18+
data_a = [cats_a[i % len(cats_a)] for i in range(SIZE)]
19+
data_b = [cats_b[i % len(cats_b)] for i in range(SIZE)]
20+
21+
s_a = pd.Categorical(data_a, categories=cats_a)
22+
s_b = pd.Categorical(data_b, categories=cats_b)
23+
24+
def cat_intersect(a, b):
25+
"""Return new Categorical with categories = intersection of a.categories and b.categories."""
26+
b_set = set(b.categories)
27+
intersected = [c for c in a.categories if c in b_set]
28+
return pd.Categorical(a, categories=intersected)
29+
30+
def cat_diff(a, b):
31+
"""Return new Categorical with categories = a.categories - b.categories."""
32+
b_set = set(b.categories)
33+
remaining = [c for c in a.categories if c not in b_set]
34+
return pd.Categorical(a, categories=remaining)
35+
36+
for _ in range(WARMUP):
37+
cat_intersect(s_a, s_b)
38+
cat_diff(s_a, s_b)
39+
40+
times = []
41+
for _ in range(ITERATIONS):
42+
t0 = time.perf_counter()
43+
cat_intersect(s_a, s_b)
44+
cat_diff(s_a, s_b)
45+
times.append((time.perf_counter() - t0) * 1000)
46+
47+
total_ms = sum(times)
48+
print(json.dumps({
49+
"function": "cat_intersect_diff",
50+
"mean_ms": total_ms / ITERATIONS,
51+
"iterations": ITERATIONS,
52+
"total_ms": total_ms,
53+
}))
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
"""
2+
Benchmark: pandas formatter factories (lambda closures applied with Series.map)
3+
Mirrors tsb's makeFloatFormatter / makePercentFormatter / makeCurrencyFormatter
4+
applied via applySeriesFormatter on a 100k-element Series.
5+
Outputs JSON: {"function": "formatter_factories", "mean_ms": ..., "iterations": ..., "total_ms": ...}
6+
"""
7+
import json
8+
import time
9+
import pandas as pd
10+
11+
SIZE = 100_000
12+
WARMUP = 5
13+
ITERATIONS = 30
14+
15+
data = [i * 0.0001234 for i in range(SIZE)]
16+
s = pd.Series(data)
17+
18+
def make_float_formatter(decimals=3):
19+
return lambda v: f"{v:.{decimals}f}" if isinstance(v, (int, float)) else str(v)
20+
21+
def make_percent_formatter(decimals=1):
22+
return lambda v: f"{v * 100:.{decimals}f}%" if isinstance(v, (int, float)) else str(v)
23+
24+
def make_currency_formatter(symbol="€", decimals=2):
25+
return lambda v: f"{symbol}{v:,.{decimals}f}" if isinstance(v, (int, float)) else str(v)
26+
27+
float_fmt = make_float_formatter(3)
28+
pct_fmt = make_percent_formatter(1)
29+
curr_fmt = make_currency_formatter("€", 2)
30+
31+
for _ in range(WARMUP):
32+
s.map(float_fmt)
33+
s.map(pct_fmt)
34+
s.map(curr_fmt)
35+
36+
times = []
37+
for _ in range(ITERATIONS):
38+
t0 = time.perf_counter()
39+
s.map(float_fmt)
40+
s.map(pct_fmt)
41+
s.map(curr_fmt)
42+
times.append((time.perf_counter() - t0) * 1000)
43+
44+
total_ms = sum(times)
45+
print(json.dumps({
46+
"function": "formatter_factories",
47+
"mean_ms": total_ms / ITERATIONS,
48+
"iterations": ITERATIONS,
49+
"total_ms": total_ms,
50+
}))
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
"""
2+
Benchmark: pandas Series.reindex() with ffill / bfill fill methods.
3+
Mirrors tsb's reindexSeries with method="ffill"/"bfill".
4+
Outputs JSON: {"function": "reindex_fill", "mean_ms": ..., "iterations": ..., "total_ms": ...}
5+
"""
6+
import json
7+
import time
8+
import pandas as pd
9+
import numpy as np
10+
11+
SIZE = 50_000
12+
WARMUP = 5
13+
ITERATIONS = 30
14+
15+
# Sparse original index: every other position
16+
orig_index = [i * 2 for i in range(SIZE)]
17+
data = [np.sin(i * 0.01) for i in range(SIZE)]
18+
s = pd.Series(data, index=orig_index)
19+
20+
# Dense new index: fills in the gaps
21+
new_index = list(range(SIZE * 2))
22+
23+
for _ in range(WARMUP):
24+
s.reindex(new_index, method="ffill")
25+
s.reindex(new_index, method="bfill")
26+
27+
times = []
28+
for _ in range(ITERATIONS):
29+
t0 = time.perf_counter()
30+
s.reindex(new_index, method="ffill")
31+
s.reindex(new_index, method="bfill")
32+
times.append((time.perf_counter() - t0) * 1000)
33+
34+
total_ms = sum(times)
35+
print(json.dumps({
36+
"function": "reindex_fill",
37+
"mean_ms": total_ms / ITERATIONS,
38+
"iterations": ITERATIONS,
39+
"total_ms": total_ms,
40+
}))
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
"""
2+
Benchmark: pandas Series.sample() with weights — weighted random sampling.
3+
Mirrors tsb's sampleSeries with weights option.
4+
Outputs JSON: {"function": "sample_weighted", "mean_ms": ..., "iterations": ..., "total_ms": ...}
5+
"""
6+
import json
7+
import time
8+
import pandas as pd
9+
10+
SIZE = 100_000
11+
N_SAMPLE = 1_000
12+
WARMUP = 5
13+
ITERATIONS = 30
14+
15+
data = [i * 0.5 for i in range(SIZE)]
16+
# Weights: higher values get more weight (triangular distribution)
17+
weights = [(i + 1) / SIZE for i in range(SIZE)]
18+
19+
s = pd.Series(data)
20+
21+
for _ in range(WARMUP):
22+
s.sample(n=N_SAMPLE, weights=weights, replace=False)
23+
24+
times = []
25+
for _ in range(ITERATIONS):
26+
t0 = time.perf_counter()
27+
s.sample(n=N_SAMPLE, weights=weights, replace=False)
28+
times.append((time.perf_counter() - t0) * 1000)
29+
30+
total_ms = sum(times)
31+
print(json.dumps({
32+
"function": "sample_weighted",
33+
"mean_ms": total_ms / ITERATIONS,
34+
"iterations": ITERATIONS,
35+
"total_ms": total_ms,
36+
}))
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
"""
2+
Benchmark: pandas Series.shift() — shift a 100k-element Series by 1, 3,
3+
and -2 periods. Mirrors tsb's shiftSeries standalone function.
4+
Outputs JSON: {"function": "series_shift_fn", "mean_ms": ..., "iterations": ..., "total_ms": ...}
5+
"""
6+
import json
7+
import time
8+
import pandas as pd
9+
10+
SIZE = 100_000
11+
WARMUP = 5
12+
ITERATIONS = 30
13+
14+
s = pd.Series([i * 0.5 for i in range(SIZE)])
15+
16+
for _ in range(WARMUP):
17+
s.shift(1)
18+
s.shift(3)
19+
s.shift(-2)
20+
21+
times = []
22+
for _ in range(ITERATIONS):
23+
t0 = time.perf_counter()
24+
s.shift(1)
25+
s.shift(3)
26+
s.shift(-2)
27+
times.append((time.perf_counter() - t0) * 1000)
28+
29+
total_ms = sum(times)
30+
print(json.dumps({
31+
"function": "series_shift_fn",
32+
"mean_ms": total_ms / ITERATIONS,
33+
"iterations": ITERATIONS,
34+
"total_ms": total_ms,
35+
}))
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/**
2+
* Benchmark: catIntersectCategories / catDiffCategories — set operations on
3+
* categorical Series categories (100k-element Series with 20 categories each).
4+
* Outputs JSON: {"function": "cat_intersect_diff", "mean_ms": ..., "iterations": ..., "total_ms": ...}
5+
*/
6+
import { Series, catIntersectCategories, catDiffCategories } from "../../src/index.ts";
7+
8+
const SIZE = 100_000;
9+
const WARMUP = 5;
10+
const ITERATIONS = 30;
11+
12+
// Build two categorical Series with overlapping but not identical category sets
13+
const catsA = Array.from({ length: 20 }, (_, i) => `cat_a_${i}`);
14+
const catsB = Array.from({ length: 20 }, (_, i) => `cat_${i < 10 ? "a" : "b"}_${i}`);
15+
16+
const dataA = Array.from({ length: SIZE }, (_, i) => catsA[i % catsA.length]);
17+
const dataB = Array.from({ length: SIZE }, (_, i) => catsB[i % catsB.length]);
18+
19+
const sA = new Series({ data: dataA }).cat.setCategories(catsA);
20+
const sB = new Series({ data: dataB }).cat.setCategories(catsB);
21+
22+
for (let i = 0; i < WARMUP; i++) {
23+
catIntersectCategories(sA, sB);
24+
catDiffCategories(sA, sB);
25+
}
26+
27+
const times: number[] = [];
28+
for (let i = 0; i < ITERATIONS; i++) {
29+
const t0 = performance.now();
30+
catIntersectCategories(sA, sB);
31+
catDiffCategories(sA, sB);
32+
times.push(performance.now() - t0);
33+
}
34+
35+
const total = times.reduce((a, b) => a + b, 0);
36+
console.log(
37+
JSON.stringify({
38+
function: "cat_intersect_diff",
39+
mean_ms: total / ITERATIONS,
40+
iterations: ITERATIONS,
41+
total_ms: total,
42+
}),
43+
);
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
/**
2+
* Benchmark: makeFloatFormatter / makePercentFormatter / makeCurrencyFormatter
3+
* — create formatter functions and apply each to a 100k-element Series.
4+
* Outputs JSON: {"function": "formatter_factories", "mean_ms": ..., "iterations": ..., "total_ms": ...}
5+
*/
6+
import {
7+
Series,
8+
makeFloatFormatter,
9+
makePercentFormatter,
10+
makeCurrencyFormatter,
11+
applySeriesFormatter,
12+
} from "../../src/index.ts";
13+
14+
const SIZE = 100_000;
15+
const WARMUP = 5;
16+
const ITERATIONS = 30;
17+
18+
const data = Array.from({ length: SIZE }, (_, i) => i * 0.0001234);
19+
const s = new Series({ data });
20+
21+
const floatFmt = makeFloatFormatter(3);
22+
const pctFmt = makePercentFormatter(1);
23+
const currFmt = makeCurrencyFormatter("€", 2);
24+
25+
for (let i = 0; i < WARMUP; i++) {
26+
applySeriesFormatter(s, floatFmt);
27+
applySeriesFormatter(s, pctFmt);
28+
applySeriesFormatter(s, currFmt);
29+
}
30+
31+
const times: number[] = [];
32+
for (let i = 0; i < ITERATIONS; i++) {
33+
const t0 = performance.now();
34+
applySeriesFormatter(s, floatFmt);
35+
applySeriesFormatter(s, pctFmt);
36+
applySeriesFormatter(s, currFmt);
37+
times.push(performance.now() - t0);
38+
}
39+
40+
const total = times.reduce((a, b) => a + b, 0);
41+
console.log(
42+
JSON.stringify({
43+
function: "formatter_factories",
44+
mean_ms: total / ITERATIONS,
45+
iterations: ITERATIONS,
46+
total_ms: total,
47+
}),
48+
);
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/**
2+
* Benchmark: reindexSeries with fill methods (ffill / bfill) — realign a
3+
* 100k-element Series to a larger index using forward-fill and backward-fill.
4+
* Extends bench_reindex which only tests the no-fill case.
5+
* Outputs JSON: {"function": "reindex_fill", "mean_ms": ..., "iterations": ..., "total_ms": ...}
6+
*/
7+
import { Series, Index, reindexSeries } from "../../src/index.ts";
8+
9+
const SIZE = 50_000;
10+
const WARMUP = 5;
11+
const ITERATIONS = 30;
12+
13+
// Sparse original index: every other position
14+
const origLabels = Array.from({ length: SIZE }, (_, i) => i * 2);
15+
const data = Array.from({ length: SIZE }, (_, i) => Math.sin(i * 0.01));
16+
const s = new Series({ data, index: new Index(origLabels) });
17+
18+
// Dense new index: fills in the gaps
19+
const newIndex = Array.from({ length: SIZE * 2 }, (_, i) => i);
20+
21+
for (let i = 0; i < WARMUP; i++) {
22+
reindexSeries(s, newIndex, { method: "ffill" });
23+
reindexSeries(s, newIndex, { method: "bfill" });
24+
}
25+
26+
const times: number[] = [];
27+
for (let i = 0; i < ITERATIONS; i++) {
28+
const t0 = performance.now();
29+
reindexSeries(s, newIndex, { method: "ffill" });
30+
reindexSeries(s, newIndex, { method: "bfill" });
31+
times.push(performance.now() - t0);
32+
}
33+
34+
const total = times.reduce((a, b) => a + b, 0);
35+
console.log(
36+
JSON.stringify({
37+
function: "reindex_fill",
38+
mean_ms: total / ITERATIONS,
39+
iterations: ITERATIONS,
40+
total_ms: total,
41+
}),
42+
);
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/**
2+
* Benchmark: sampleSeries with weights — weighted random sampling from a
3+
* 100k-element Series. Extends bench_sample_fn which tests unweighted sampling.
4+
* Outputs JSON: {"function": "sample_weighted", "mean_ms": ..., "iterations": ..., "total_ms": ...}
5+
*/
6+
import { Series, sampleSeries } from "../../src/index.ts";
7+
8+
const SIZE = 100_000;
9+
const N_SAMPLE = 1_000;
10+
const WARMUP = 5;
11+
const ITERATIONS = 30;
12+
13+
const data = Array.from({ length: SIZE }, (_, i) => i * 0.5);
14+
// Weights: higher values get more weight (triangular distribution)
15+
const weights = Array.from({ length: SIZE }, (_, i) => (i + 1) / SIZE);
16+
17+
const s = new Series({ data });
18+
19+
for (let i = 0; i < WARMUP; i++) {
20+
sampleSeries(s, { n: N_SAMPLE, weights });
21+
}
22+
23+
const times: number[] = [];
24+
for (let i = 0; i < ITERATIONS; i++) {
25+
const t0 = performance.now();
26+
sampleSeries(s, { n: N_SAMPLE, weights });
27+
times.push(performance.now() - t0);
28+
}
29+
30+
const total = times.reduce((a, b) => a + b, 0);
31+
console.log(
32+
JSON.stringify({
33+
function: "sample_weighted",
34+
mean_ms: total / ITERATIONS,
35+
iterations: ITERATIONS,
36+
total_ms: total,
37+
}),
38+
);

0 commit comments

Comments
 (0)