Skip to content

Commit d235034

Browse files
Iteration 70: Cumulative operations (cumsum/cumprod/cummax/cummin)
- Added src/stats/cum_ops.ts — cumsum(), cumprod(), cummax(), cummin() for Series - Added dataFrameCumsum(), dataFrameCumprod(), dataFrameCummax(), dataFrameCummin() - skipna option (default true): NaN/null positions return NaN/null, accumulator skips them - skipna: false: any missing value poisons all subsequent results - axis=0 (default): column-wise; axis=1: row-wise across columns - Non-numeric values treated as missing in cumsum/cumprod - cummax/cummin work on any comparable scalar (numbers, strings, booleans) - 50+ unit tests + 6 property-based tests (fast-check) - Playground: playground/cum_ops.html (8 interactive sections) Run: https://github.com/githubnext/tsessebe/actions/runs/24009460051 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent b3ad0f3 commit d235034

3 files changed

Lines changed: 988 additions & 0 deletions

File tree

playground/cum_ops.html

Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
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 — cumulative operations</title>
7+
<script src="playground-runtime.js"></script>
8+
<style>
9+
body { font-family: system-ui, sans-serif; max-width: 860px; margin: 2rem auto; padding: 0 1rem; color: #1a1a1a; }
10+
h1 { font-size: 2rem; margin-bottom: .25rem; }
11+
.subtitle { color: #555; margin-bottom: 2rem; }
12+
section { margin-bottom: 2.5rem; }
13+
h2 { font-size: 1.25rem; border-bottom: 2px solid #e0e0e0; padding-bottom: .35rem; margin-bottom: 1rem; }
14+
pre { background: #f5f5f5; border-radius: 6px; padding: 1rem; overflow-x: auto; font-size: .875rem; }
15+
code { font-family: "JetBrains Mono", "Fira Code", monospace; }
16+
.output { background: #eaf7ea; border-left: 4px solid #4caf50; padding: .75rem 1rem; border-radius: 0 6px 6px 0; margin-top: .5rem; font-size: .875rem; white-space: pre; font-family: monospace; }
17+
.note { background: #fff8e1; border-left: 4px solid #ffc107; padding: .6rem 1rem; border-radius: 0 6px 6px 0; font-size: .9rem; margin: .75rem 0; }
18+
nav { margin-bottom: 1.5rem; font-size: .9rem; color: #555; }
19+
nav a { color: #0366d6; text-decoration: none; }
20+
nav a:hover { text-decoration: underline; }
21+
</style>
22+
</head>
23+
<body>
24+
<nav><a href="index.html">← tsb playground</a></nav>
25+
<h1>cumulative operations</h1>
26+
<p class="subtitle">
27+
Compute running totals, products, maxima, and minima —
28+
mirrors <code>pandas.Series.cumsum()</code> / <code>cumprod()</code> / <code>cummax()</code> / <code>cummin()</code>.
29+
</p>
30+
31+
<section>
32+
<h2>1 — cumsum: running total</h2>
33+
<p>
34+
<code>cumsum(series)</code> returns a new Series where each value is the sum of all preceding
35+
values plus the current one. Mirrors <code>pandas.Series.cumsum()</code>.
36+
</p>
37+
<pre><code id="ex1-code">import { Series, cumsum } from "tsb";
38+
39+
const s = new Series({ data: [1, 2, 3, 4, 5] });
40+
const cs = cumsum(s);
41+
console.log([...cs.values]); // [1, 3, 6, 10, 15]
42+
</code></pre>
43+
<div class="output" id="ex1-output">Loading…</div>
44+
</section>
45+
46+
<section>
47+
<h2>2 — cumprod: running product</h2>
48+
<p>
49+
<code>cumprod(series)</code> returns a new Series where each value is the product of all values
50+
up to and including that position. Mirrors <code>pandas.Series.cumprod()</code>.
51+
</p>
52+
<pre><code id="ex2-code">import { Series, cumprod } from "tsb";
53+
54+
const s = new Series({ data: [1, 2, 3, 4, 5] });
55+
const cp = cumprod(s);
56+
console.log([...cp.values]); // [1, 2, 6, 24, 120]
57+
</code></pre>
58+
<div class="output" id="ex2-output">Loading…</div>
59+
</section>
60+
61+
<section>
62+
<h2>3 — cummax and cummin</h2>
63+
<p>
64+
<code>cummax(series)</code> tracks the running maximum; <code>cummin(series)</code> tracks the
65+
running minimum. Both work on numbers, strings, and booleans.
66+
</p>
67+
<pre><code id="ex3-code">import { Series, cummax, cummin } from "tsb";
68+
69+
const s = new Series({ data: [3, 1, 4, 1, 5, 9, 2, 6] });
70+
console.log([...cummax(s).values]); // [3, 3, 4, 4, 5, 9, 9, 9]
71+
console.log([...cummin(s).values]); // [3, 1, 1, 1, 1, 1, 1, 1]
72+
</code></pre>
73+
<div class="output" id="ex3-output">Loading…</div>
74+
</section>
75+
76+
<section>
77+
<h2>4 — Handling missing values (skipna)</h2>
78+
<p>
79+
By default <code>skipna: true</code>: missing values return <code>NaN</code>/<code>null</code>
80+
in the result but do <em>not</em> affect the running accumulator.
81+
With <code>skipna: false</code>, any missing value poisons all subsequent results.
82+
</p>
83+
<pre><code id="ex4-code">import { Series, cumsum } from "tsb";
84+
85+
const s = new Series({ data: [1, null, 3, 4] });
86+
87+
// skipna: true (default) — NaN at position 1, accumulator continues
88+
const skipTrue = cumsum(s);
89+
console.log([...skipTrue.values]); // [1, NaN, 4, 8]
90+
91+
// skipna: false — NaN at position 1 poisons everything after
92+
const skipFalse = cumsum(s, { skipna: false });
93+
console.log([...skipFalse.values]); // [1, NaN, NaN, NaN]
94+
</code></pre>
95+
<div class="output" id="ex4-output">Loading…</div>
96+
</section>
97+
98+
<section>
99+
<h2>5 — DataFrame: axis=0 (column-wise)</h2>
100+
<p>
101+
<code>dataFrameCumsum(df)</code> applies the operation independently to each column
102+
(axis=0 is the default, same as pandas).
103+
</p>
104+
<pre><code id="ex5-code">import { DataFrame, dataFrameCumsum, dataFrameCummax } from "tsb";
105+
106+
const df = DataFrame.fromColumns({
107+
revenue: [100, 150, 200, 120],
108+
cost: [60, 80, 110, 70],
109+
});
110+
111+
const csDf = dataFrameCumsum(df);
112+
console.log([...csDf.col("revenue").values]); // [100, 250, 450, 570]
113+
console.log([...csDf.col("cost").values]); // [60, 140, 250, 320]
114+
115+
const cmDf = dataFrameCummax(df);
116+
console.log([...cmDf.col("revenue").values]); // [100, 150, 200, 200]
117+
</code></pre>
118+
<div class="output" id="ex5-output">Loading…</div>
119+
</section>
120+
121+
<section>
122+
<h2>6 — DataFrame: axis=1 (row-wise)</h2>
123+
<p>
124+
With <code>axis: 1</code> (or <code>axis: "columns"</code>), the operation is applied
125+
across columns for each row — each cell becomes the cumulative value of all columns
126+
to its left plus itself.
127+
</p>
128+
<pre><code id="ex6-code">import { DataFrame, dataFrameCumsum } from "tsb";
129+
130+
const df = DataFrame.fromColumns({
131+
q1: [10, 20, 30],
132+
q2: [15, 25, 35],
133+
q3: [12, 22, 32],
134+
});
135+
136+
// axis=1: running total across quarters for each year-row
137+
const ytd = dataFrameCumsum(df, { axis: 1 });
138+
console.log([...ytd.col("q1").values]); // [10, 20, 30] (unchanged — first col)
139+
console.log([...ytd.col("q2").values]); // [25, 45, 65] (q1+q2)
140+
console.log([...ytd.col("q3").values]); // [37, 67, 97] (q1+q2+q3)
141+
</code></pre>
142+
<div class="output" id="ex6-output">Loading…</div>
143+
</section>
144+
145+
<section>
146+
<h2>7 — Real-world example: portfolio tracking</h2>
147+
<p>
148+
Track the running portfolio value and the running drawdown (how far we are from the
149+
all-time high) using <code>cumsum</code> and <code>cummax</code>.
150+
</p>
151+
<pre><code id="ex7-code">import { Series, cumsum, cummax } from "tsb";
152+
153+
// Daily P&L in dollars
154+
const pnl = new Series({ data: [200, -50, 300, -150, 400, -80, 600] });
155+
156+
const equity = cumsum(pnl);
157+
const peak = cummax(equity);
158+
159+
// Drawdown: how far below the all-time high we currently are
160+
const drawdown = new Series({
161+
data: equity.values.map((v, i) => {
162+
const p = peak.values[i];
163+
return (typeof v === "number" && typeof p === "number") ? v - p : null;
164+
}),
165+
});
166+
167+
console.log("Equity curve: ", [...equity.values]);
168+
// [200, 150, 450, 300, 700, 620, 1220]
169+
console.log("All-time high:", [...peak.values]);
170+
// [200, 200, 450, 450, 700, 700, 1220]
171+
console.log("Drawdown: ", [...drawdown.values]);
172+
// [0, -50, 0, -150, 0, -80, 0]
173+
</code></pre>
174+
<div class="output" id="ex7-output">Loading…</div>
175+
</section>
176+
177+
<section>
178+
<h2>8 — String series: lexicographic cummax / cummin</h2>
179+
<p>
180+
<code>cummax</code> and <code>cummin</code> work on any comparable type, including
181+
strings (lexicographic ordering).
182+
</p>
183+
<pre><code id="ex8-code">import { Series, cummax, cummin } from "tsb";
184+
185+
const words = new Series({ data: ["banana", "apple", "cherry", "apricot", "date"] });
186+
console.log([...cummax(words).values]);
187+
// ["banana", "banana", "cherry", "cherry", "cherry"]
188+
console.log([...cummin(words).values]);
189+
// ["banana", "apple", "apple", "apple", "apple"]
190+
</code></pre>
191+
<div class="output" id="ex8-output">Loading…</div>
192+
</section>
193+
194+
</body>
195+
</html>

0 commit comments

Comments
 (0)