|
| 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 & 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 & 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 < 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> |
0 commit comments