Skip to content

Commit 8b6229d

Browse files
Iteration 146: pipe_apply — functional pipeline and apply utilities
Added `src/core/pipe_apply.ts` — standalone pipe/apply functions: - `pipe` — variadic type-safe functional pipeline (8 overloads, works on any value) - `seriesApply` — element-wise apply with (value, label, position) context - `seriesTransform` — scalar→scalar element-wise transform - `dataFrameApply` — column-wise (axis=0) or row-wise (axis=1) aggregation → Series - `dataFrameApplyMap` — element-wise apply to every cell → DataFrame (pandas applymap) - `dataFrameTransform` — column-wise transform, each col → Series of same length - `dataFrameTransformRows` — row-wise transform, partial updates supported Also added 50+ unit tests + 4 property-based tests and playground page `pipe_apply.html`. Metric: 38 → 39 (+1 exported source file) Run: https://github.com/githubnext/tsessebe/actions/runs/24204988345 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent fa9a314 commit 8b6229d

6 files changed

Lines changed: 1056 additions & 0 deletions

File tree

playground/index.html

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -299,6 +299,16 @@ <h3><a href="attrs.html" style="color: var(--accent); text-decoration: none;">
299299
<p>Attach arbitrary key→value metadata to any <code>Series</code> or <code>DataFrame</code> via a <strong>WeakMap registry</strong>. Provides <code>getAttrs</code>, <code>setAttrs</code>, <code>updateAttrs</code>, <code>copyAttrs</code>, <code>withAttrs</code>, <code>mergeAttrs</code>, <code>clearAttrs</code>, <code>getAttr</code>, <code>setAttr</code>, <code>deleteAttr</code>, <code>attrsCount</code>, <code>attrsKeys</code>. Mirrors <code>pandas.DataFrame.attrs</code> / <code>pandas.Series.attrs</code>.</p>
300300
<div class="status done">✅ Complete</div>
301301
</div>
302+
<div class="feature-card">
303+
<h3><a href="string_ops.html" style="color: var(--accent); text-decoration: none;">🔤 string_ops — Standalone String Ops</a></h3>
304+
<p>Module-level string utilities: <code>strNormalize</code> (Unicode NFC/NFD/NFKC/NFKD), <code>strGetDummies</code> (one-hot DataFrame), <code>strExtractAll</code> (all regex matches), <code>strRemovePrefix</code>, <code>strRemoveSuffix</code>, <code>strTranslate</code> (char-level substitution), <code>strCharWidth</code> (CJK-aware display width), <code>strByteLength</code>. Works on Series, arrays, or scalars.</p>
305+
<div class="status done">✅ Complete</div>
306+
</div>
307+
<div class="feature-card">
308+
<h3><a href="pipe_apply.html" style="color: var(--accent); text-decoration: none;">🔗 pipe_apply — Pipeline &amp; Apply Utilities</a></h3>
309+
<p>Standalone equivalents of pandas' <code>pipe()</code> / <code>apply()</code> / <code>applymap()</code>: <code>pipe</code> (variadic type-safe pipeline), <code>seriesApply</code> (element-wise with label/pos context), <code>seriesTransform</code>, <code>dataFrameApply</code> (axis 0/1), <code>dataFrameApplyMap</code> (cell-wise), <code>dataFrameTransform</code> (column-wise), <code>dataFrameTransformRows</code> (row-wise).</p>
310+
<div class="status done">✅ Complete</div>
311+
</div>
302312
</div>
303313
</section>
304314
</main>

playground/pipe_apply.html

Lines changed: 276 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,276 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8" />
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
6+
<title>tsb — pipe_apply: functional pipeline &amp; apply utilities</title>
7+
<style>
8+
body { font-family: system-ui, sans-serif; max-width: 860px; margin: 2rem auto; padding: 0 1rem; line-height: 1.6; color: #1a1a1a; }
9+
h1 { color: #0066cc; }
10+
h2 { color: #333; border-bottom: 1px solid #eee; padding-bottom: 0.3em; }
11+
pre { background: #f6f8fa; border-radius: 6px; padding: 1rem; overflow-x: auto; font-size: 0.9em; }
12+
code { font-family: 'Fira Code', 'Cascadia Code', monospace; }
13+
.note { background: #fffbea; border-left: 4px solid #f5a623; padding: 0.7rem 1rem; border-radius: 0 6px 6px 0; margin: 1rem 0; }
14+
table { border-collapse: collapse; width: 100%; margin: 1rem 0; }
15+
th, td { border: 1px solid #ddd; padding: 0.5rem 0.75rem; text-align: left; }
16+
th { background: #f0f4f8; }
17+
a { color: #0066cc; }
18+
.api-table td:first-child { font-family: monospace; white-space: nowrap; }
19+
</style>
20+
</head>
21+
<body>
22+
<p><a href="index.html">← tsb playground</a></p>
23+
24+
<h1><code>pipe_apply</code> — Functional Pipeline &amp; Apply Utilities</h1>
25+
<p>
26+
Standalone equivalents of the pandas
27+
<a href="https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.pipe.html" target="_blank"><code>DataFrame.pipe()</code></a>
28+
/
29+
<a href="https://pandas.pydata.org/docs/reference/api/pandas.Series.pipe.html" target="_blank"><code>Series.pipe()</code></a>
30+
chaining pattern plus various
31+
<a href="https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.apply.html" target="_blank"><code>apply()</code></a>
32+
/
33+
<a href="https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.applymap.html" target="_blank"><code>applymap()</code></a>
34+
operations — usable without method-call syntax.
35+
</p>
36+
37+
<div class="note">
38+
<strong>Why standalone?</strong> pandas chains operations via methods:
39+
<code>df.pipe(fn1).pipe(fn2)</code>. tsb provides a module-level
40+
<code>pipe(value, fn1, fn2, …)</code> that works on <em>any</em> value,
41+
not just DataFrames. All functions are <strong>pure</strong> — inputs are never mutated.
42+
</div>
43+
44+
<h2>API Summary</h2>
45+
<table class="api-table">
46+
<thead>
47+
<tr><th>Function</th><th>Pandas equivalent</th><th>Description</th></tr>
48+
</thead>
49+
<tbody>
50+
<tr>
51+
<td>pipe(value, fn1, fn2, …)</td>
52+
<td>df.pipe(fn).pipe(fn2)</td>
53+
<td>Variadic type-safe pipeline — passes value through fns left-to-right</td>
54+
</tr>
55+
<tr>
56+
<td>seriesApply(s, fn)</td>
57+
<td>s.apply(fn)</td>
58+
<td>Element-wise; fn receives (value, label, position)</td>
59+
</tr>
60+
<tr>
61+
<td>seriesTransform(s, fn)</td>
62+
<td>s.transform(fn)</td>
63+
<td>Element-wise scalar→scalar; simpler than seriesApply</td>
64+
</tr>
65+
<tr>
66+
<td>dataFrameApply(df, fn, axis?)</td>
67+
<td>df.apply(fn, axis=0|1)</td>
68+
<td>Apply fn to each column (axis=0) or row (axis=1) → Series of results</td>
69+
</tr>
70+
<tr>
71+
<td>dataFrameApplyMap(df, fn)</td>
72+
<td>df.applymap(fn) / df.map(fn)</td>
73+
<td>Apply fn to every cell; fn receives (value, rowLabel, colName)</td>
74+
</tr>
75+
<tr>
76+
<td>dataFrameTransform(df, fn)</td>
77+
<td>df.transform(fn)</td>
78+
<td>Replace each column with fn(col) — must return same-length Series</td>
79+
</tr>
80+
<tr>
81+
<td>dataFrameTransformRows(df, fn)</td>
82+
<td>df.apply(fn, axis=1, result_type='expand')</td>
83+
<td>Replace each row with fn(rowRecord) — partial updates allowed</td>
84+
</tr>
85+
</tbody>
86+
</table>
87+
88+
<h2>pipe — functional pipeline</h2>
89+
90+
<pre><code>import { pipe } from "tsb";
91+
import { DataFrame } from "tsb";
92+
93+
// Type-safe pipeline with up to 8 steps (return type inferred at each step)
94+
const result = pipe(
95+
rawData,
96+
(df) => df.dropna(), // DataFrame → DataFrame
97+
(df) => df.assign({ z: df.col("x").add(df.col("y")).values }), // DataFrame → DataFrame
98+
(df) => df.head(10), // DataFrame → DataFrame
99+
(df) => df.sum(), // DataFrame → Series
100+
);
101+
102+
// Works on any value — not just DataFrames
103+
const n = pipe(
104+
3,
105+
(x) => x + 1, // 4
106+
(x) => x * x, // 16
107+
(x) => x - 1, // 15
108+
);
109+
// n === 15</code></pre>
110+
111+
<h2>seriesApply — element-wise apply</h2>
112+
113+
<pre><code>import { seriesApply, seriesTransform } from "tsb";
114+
import { Series } from "tsb";
115+
116+
const temps = new Series({ data: [22.1, 23.5, null, 21.8], name: "temp_C" });
117+
118+
// Element-wise with (value, label, position) context
119+
const fahrenheit = seriesApply(temps, (v) => v === null ? null : (v as number) * 9/5 + 32);
120+
// [71.78, 74.3, null, 71.24]
121+
122+
// Simple scalar transform (no label/position needed)
123+
const rounded = seriesTransform(temps, (v) => v === null ? null : Math.round(v as number));
124+
// [22, 24, null, 22]
125+
126+
// Using position to build cumulative logic
127+
const withPos = seriesApply(
128+
new Series({ data: [10, 20, 30] }),
129+
(v, _label, pos) => (v as number) + pos * 100,
130+
);
131+
// [10, 120, 230]</code></pre>
132+
133+
<h2>dataFrameApply — column/row aggregation</h2>
134+
135+
<pre><code>import { dataFrameApply } from "tsb";
136+
import { DataFrame } from "tsb";
137+
138+
const df = DataFrame.fromColumns({
139+
score: [85, 92, 78, 95],
140+
weight: [1.0, 1.2, 0.8, 1.5],
141+
});
142+
143+
// axis=0 (default): apply fn to each column → Series indexed by column names
144+
const colMax = dataFrameApply(df, (col) => col.max() ?? null);
145+
// colMax.at("score") === 95
146+
// colMax.at("weight") === 1.5
147+
148+
// axis=1: apply fn to each row → Series indexed by row labels
149+
const weightedScore = dataFrameApply(
150+
df,
151+
(row) => (row.at("score") as number) * (row.at("weight") as number),
152+
1,
153+
);
154+
// [85, 110.4, 62.4, 142.5]</code></pre>
155+
156+
<h2>dataFrameApplyMap — element-wise cell transform</h2>
157+
158+
<pre><code>import { dataFrameApplyMap } from "tsb";
159+
import { DataFrame } from "tsb";
160+
161+
const df = DataFrame.fromColumns({
162+
a: [1, -2, 3],
163+
b: [-4, 5, -6],
164+
});
165+
166+
// Zero out all negative values (like pandas df.applymap(lambda x: max(x, 0)))
167+
const clipped = dataFrameApplyMap(df, (v) => {
168+
return typeof v === "number" && v < 0 ? 0 : v;
169+
});
170+
// a: [1, 0, 3]
171+
// b: [0, 5, 0]
172+
173+
// fn receives full context: (value, rowLabel, colName)
174+
const tagged = dataFrameApplyMap(df, (v, row, col) => `${col}[${row}]=${v}`);
175+
// a: ["a[0]=1", "a[1]=-2", "a[2]=3"]
176+
// b: ["b[0]=-4", "b[1]=5", "b[2]=-6"]</code></pre>
177+
178+
<h2>dataFrameTransform — column-wise transform</h2>
179+
180+
<pre><code>import { dataFrameTransform, seriesTransform } from "tsb";
181+
import { DataFrame } from "tsb";
182+
183+
const df = DataFrame.fromColumns({
184+
x: [1, 2, 3, 4, 5],
185+
y: [10, 20, 30, 40, 50],
186+
});
187+
188+
// Z-score normalize each column
189+
const normalized = dataFrameTransform(df, (col) => {
190+
const mu = col.mean();
191+
const sd = col.std();
192+
return seriesTransform(col, (v) =>
193+
typeof v === "number" && sd > 0 ? (v - mu) / sd : v
194+
);
195+
});
196+
197+
// Bin each column into quartiles
198+
const binned = dataFrameTransform(df, (col) => {
199+
const q1 = col.quantile(0.25);
200+
const q2 = col.quantile(0.5);
201+
const q3 = col.quantile(0.75);
202+
return seriesTransform(col, (v) => {
203+
const n = v as number;
204+
if (n <= q1) return "Q1";
205+
if (n <= q2) return "Q2";
206+
if (n <= q3) return "Q3";
207+
return "Q4";
208+
});
209+
});</code></pre>
210+
211+
<h2>dataFrameTransformRows — row-wise transform</h2>
212+
213+
<pre><code>import { dataFrameTransformRows } from "tsb";
214+
import { DataFrame } from "tsb";
215+
216+
const df = DataFrame.fromColumns({
217+
first: ["alice", "bob", "carol"],
218+
last: ["smith", "jones", "white"],
219+
score: [88, 75, 92],
220+
});
221+
222+
// Normalise scores relative to the row's position (illustrative)
223+
const updated = dataFrameTransformRows(df, (row, _label, pos) => ({
224+
// Only return keys you want to change — others are preserved as-is
225+
score: (row["score"] as number) + pos,
226+
}));
227+
// scores become [88, 76, 94]
228+
// first and last columns are unchanged
229+
230+
// Full row transformation (compute full name)
231+
const withFull = dataFrameTransformRows(df, (row) => ({
232+
first: row["first"],
233+
last: row["last"],
234+
score: row["score"],
235+
full: `${row["first"]} ${row["last"]}`,
236+
}));</code></pre>
237+
238+
<h2>Combining pipe + apply</h2>
239+
240+
<pre><code>import { pipe, dataFrameApplyMap, dataFrameTransform, seriesTransform } from "tsb";
241+
import { DataFrame } from "tsb";
242+
243+
const raw = DataFrame.fromColumns({
244+
price: [9.99, -1, 24.5, null, 49.0],
245+
quantity: [3, 5, null, 2, 1],
246+
});
247+
248+
// Clean → impute → normalise in one readable pipeline
249+
const clean = pipe(
250+
raw,
251+
// 1. zero out invalid prices/quantities
252+
(df) => dataFrameApplyMap(df, (v) =>
253+
v === null || (typeof v === "number" && v &lt; 0) ? 0 : v
254+
),
255+
// 2. add derived revenue column
256+
(df) => df.assign({
257+
revenue: df.col("price").mul(df.col("quantity")).values,
258+
}),
259+
// 3. round everything to 2 dp
260+
(df) => dataFrameTransform(df, (col) =>
261+
seriesTransform(col, (v) =>
262+
typeof v === "number" ? Math.round(v * 100) / 100 : v
263+
)
264+
),
265+
);</code></pre>
266+
267+
<hr />
268+
<p>
269+
<a href="https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.pipe.html" target="_blank">pandas DataFrame.pipe docs</a>
270+
·
271+
<a href="https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.apply.html" target="_blank">pandas DataFrame.apply docs</a>
272+
·
273+
<a href="https://github.com/githubnext/tsessebe" target="_blank">tsb on GitHub</a>
274+
</p>
275+
</body>
276+
</html>

src/core/index.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,3 +41,12 @@ export {
4141
mergeAttrs,
4242
} from "./attrs.ts";
4343
export type { Attrs } from "./attrs.ts";
44+
export {
45+
pipe,
46+
seriesApply,
47+
seriesTransform,
48+
dataFrameApply,
49+
dataFrameApplyMap,
50+
dataFrameTransform,
51+
dataFrameTransformRows,
52+
} from "./pipe_apply.ts";

0 commit comments

Comments
 (0)