Skip to content

Commit a98d36f

Browse files
Iteration 141: Add seriesWhere/seriesMask/dataFrameWhere/dataFrameMask
Run: https://github.com/githubnext/tsessebe/actions/runs/24177038823 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent d84b535 commit a98d36f

6 files changed

Lines changed: 866 additions & 0 deletions

File tree

playground/index.html

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,11 @@ <h3><a href="window_extended.html" style="color: var(--accent); text-decoration:
279279
<p>Higher-order rolling window statistics: <code>rollingSem</code> (standard error of mean), <code>rollingSkew</code> (Fisher-Pearson skewness), <code>rollingKurt</code> (excess kurtosis), and <code>rollingQuantile</code> (arbitrary percentile with 5 interpolation methods). Mirrors <code>pandas.Series.rolling().sem/skew/kurt/quantile()</code>.</p>
280280
<div class="status done">✅ Complete</div>
281281
</div>
282+
<div class="feature-card">
283+
<h3><a href="where_mask.html" style="color: var(--accent); text-decoration: none;">🎭 where / mask</a></h3>
284+
<p>Element-wise conditional selection: <code>seriesWhere</code> / <code>seriesMask</code> and <code>dataFrameWhere</code> / <code>dataFrameMask</code>. Accepts boolean arrays, label-aligned boolean Series/DataFrame, or callables. Mirrors <code>pandas.Series.where</code>, <code>pandas.DataFrame.where</code>, and their <code>.mask()</code> inverses.</p>
285+
<div class="status done">✅ Complete</div>
286+
</div>
282287
</div>
283288
</section>
284289
</main>

playground/where_mask.html

Lines changed: 220 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,220 @@
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 — where / mask: Conditional Selection</title>
7+
<style>
8+
body {
9+
font-family: system-ui, sans-serif;
10+
max-width: 860px;
11+
margin: 2rem auto;
12+
padding: 0 1rem;
13+
line-height: 1.6;
14+
color: #1a1a1a;
15+
}
16+
h1 { color: #0d47a1; }
17+
h2 { color: #1565c0; border-bottom: 2px solid #e3f2fd; padding-bottom: 0.25rem; }
18+
pre {
19+
background: #f5f5f5;
20+
border-left: 4px solid #0d47a1;
21+
padding: 1rem;
22+
overflow-x: auto;
23+
border-radius: 4px;
24+
}
25+
code { font-family: "Fira Code", "Cascadia Code", monospace; font-size: 0.9em; }
26+
.demo {
27+
background: #e8f5e9;
28+
border: 1px solid #a5d6a7;
29+
border-radius: 6px;
30+
padding: 1rem 1.25rem;
31+
margin: 1rem 0;
32+
}
33+
.demo h3 { margin-top: 0; color: #2e7d32; }
34+
table { border-collapse: collapse; width: 100%; margin: 0.5rem 0; }
35+
th, td { border: 1px solid #ccc; padding: 0.4rem 0.75rem; text-align: left; }
36+
th { background: #e3f2fd; }
37+
.note { background: #fff9c4; border: 1px solid #f9a825; border-radius: 4px; padding: 0.75rem 1rem; }
38+
a { color: #0d47a1; }
39+
</style>
40+
</head>
41+
<body>
42+
<h1>tsb — <code>where</code> / <code>mask</code>: Conditional Selection</h1>
43+
<p>
44+
<code>seriesWhere</code> / <code>seriesMask</code> and their DataFrame equivalents
45+
allow element-wise conditional replacement — the TypeScript equivalents of
46+
<a href="https://pandas.pydata.org/docs/reference/api/pandas.Series.where.html"><code>pandas.Series.where</code></a>
47+
and
48+
<a href="https://pandas.pydata.org/docs/reference/api/pandas.Series.mask.html"><code>pandas.Series.mask</code></a>.
49+
</p>
50+
51+
<div class="note">
52+
<strong>Quick rule:</strong><br />
53+
<code>where(cond)</code><em>keep</em> where <code>cond</code> is <strong>true</strong>, replace elsewhere.<br />
54+
<code>mask(cond)</code><em>keep</em> where <code>cond</code> is <strong>false</strong>, replace elsewhere.<br />
55+
They are exact inverses of each other.
56+
</div>
57+
58+
<h2>1. <code>seriesWhere</code> — Boolean Array Condition</h2>
59+
<p>
60+
Pass a <code>boolean[]</code> to keep values at <code>true</code> positions, replace
61+
the rest with <code>null</code> (or a custom <code>other</code> value).
62+
</p>
63+
<pre><code>import { Series, seriesWhere } from "tsb";
64+
65+
const scores = new Series({ data: [42, 91, 67, 55, 88] });
66+
const highScores = seriesWhere(scores, [false, true, false, false, true]);
67+
// Series [null, 91, null, null, 88]
68+
69+
// Custom replacement value
70+
const clamped = seriesWhere(scores, [false, true, false, false, true], { other: 0 });
71+
// Series [0, 91, 0, 0, 88]</code></pre>
72+
73+
<h2>2. <code>seriesWhere</code> — Callable Condition</h2>
74+
<p>
75+
Pass a function that receives the Series and returns a <code>boolean[]</code> or
76+
<code>Series&lt;boolean&gt;</code>. This avoids computing the condition array manually.
77+
</p>
78+
<pre><code>import { Series, seriesWhere } from "tsb";
79+
80+
const temps = new Series({ data: [-5, 12, 23, -3, 8] });
81+
82+
// Keep only values above freezing
83+
const aboveFreezing = seriesWhere(
84+
temps,
85+
(s) =&gt; s.values.map((v) =&gt; (v as number) &gt; 0),
86+
);
87+
// Series [null, 12, 23, null, 8]
88+
89+
// Replace with 0 instead of null
90+
const noFreeze = seriesWhere(
91+
temps,
92+
(s) =&gt; s.values.map((v) =&gt; (v as number) &gt; 0),
93+
{ other: 0 },
94+
);
95+
// Series [0, 12, 23, 0, 8]</code></pre>
96+
97+
<h2>3. <code>seriesMask</code> — The Inverse</h2>
98+
<p>
99+
<code>mask</code> replaces positions where the condition is <strong>true</strong>
100+
(the opposite of <code>where</code>). Use it to "blank out" outliers or invalid values.
101+
</p>
102+
<pre><code>import { Series, seriesMask } from "tsb";
103+
104+
const data = new Series({ data: [1, 2, 3, 4, 5] });
105+
106+
// Mask out values greater than 3
107+
const masked = seriesMask(
108+
data,
109+
(s) =&gt; s.values.map((v) =&gt; (v as number) &gt; 3),
110+
{ other: null },
111+
);
112+
// Series [1, 2, 3, null, null]</code></pre>
113+
114+
<h2>4. <code>dataFrameWhere</code> — Element-Wise on DataFrames</h2>
115+
<p>
116+
Pass a boolean <code>DataFrame</code> or a callable that returns one.
117+
Columns and row labels are aligned by name.
118+
</p>
119+
<pre><code>import { DataFrame, dataFrameWhere } from "tsb";
120+
121+
const df = DataFrame.fromColumns({
122+
temp_c: [22, -3, 18, -7, 30],
123+
humidity: [55, 80, 62, 75, 45],
124+
});
125+
126+
// Keep only valid summer readings (temp &gt; 0)
127+
const condDf = DataFrame.fromColumns({
128+
temp_c: [true, false, true, false, true],
129+
humidity: [true, false, true, false, true],
130+
});
131+
132+
const summer = dataFrameWhere(df, condDf);
133+
// DataFrame:
134+
// temp_c [22, null, 18, null, 30 ]
135+
// humidity [55, null, 62, null, 45 ]</code></pre>
136+
137+
<h2>5. <code>dataFrameWhere</code> — Callable Condition</h2>
138+
<pre><code>import { DataFrame, dataFrameWhere } from "tsb";
139+
140+
const df = DataFrame.fromColumns({
141+
a: [1, 2, 3, 4, 5],
142+
b: [10, 20, 30, 40, 50],
143+
});
144+
145+
// Keep only values &gt; 2 (column-wise threshold)
146+
const result = dataFrameWhere(df, (d) =&gt; {
147+
const condCols: Record&lt;string, boolean[]&gt; = {};
148+
for (const col of d.columns) {
149+
condCols[col as string] = d.col(col as string).values.map(
150+
(v) =&gt; (v as number) &gt; 2
151+
);
152+
}
153+
return DataFrame.fromColumns(condCols);
154+
});
155+
// DataFrame:
156+
// a: [null, null, 3, 4, 5]
157+
// b: [10, 20, 30, 40, 50]</code></pre>
158+
159+
<h2>6. <code>dataFrameMask</code> — DataFrame Mask</h2>
160+
<pre><code>import { DataFrame, dataFrameMask } from "tsb";
161+
162+
const df = DataFrame.fromColumns({
163+
sales: [100, 200, 50, 300, 80],
164+
profit: [10, 40, -5, 60, -2],
165+
});
166+
167+
// Mask out (replace) rows with negative profit
168+
const cleaned = dataFrameMask(
169+
df,
170+
(d) =&gt; {
171+
const condCols: Record&lt;string, boolean[]&gt; = {};
172+
for (const col of d.columns) {
173+
condCols[col as string] = d.col(col as string).values.map(
174+
(v) =&gt; (v as number) &lt; 0
175+
);
176+
}
177+
return DataFrame.fromColumns(condCols);
178+
},
179+
{ other: 0 },
180+
);
181+
// DataFrame:
182+
// sales: [100, 200, 50, 300, 80]
183+
// profit: [10, 40, 0, 60, 0 ]</code></pre>
184+
185+
<h2>Label-Aligned Series Condition</h2>
186+
<p>
187+
When you pass a <code>Series&lt;boolean&gt;</code> as the condition, values are aligned
188+
by <strong>label</strong>, not position. Labels absent from the condition series are treated
189+
as <code>false</code>.
190+
</p>
191+
<pre><code>import { Series, seriesWhere } from "tsb";
192+
193+
const prices = new Series({ data: [10, 20, 30], index: ["a", "b", "c"] });
194+
const valid = new Series&lt;boolean&gt;({ data: [false, true], index: ["a", "b"] });
195+
196+
// Only "b" is in the condition with value=true; "a"=false, "c" missing→false
197+
const result = seriesWhere(prices, valid, { other: -1 });
198+
// Series { a: -1, b: 20, c: -1 }</code></pre>
199+
200+
<h2>API Reference</h2>
201+
<table>
202+
<tr><th>Function</th><th>Keeps when cond is…</th><th>Replaces with</th></tr>
203+
<tr><td><code>seriesWhere(s, cond, {other})</code></td><td><code>true</code></td><td><code>other</code> (default <code>null</code>)</td></tr>
204+
<tr><td><code>seriesMask(s, cond, {other})</code></td><td><code>false</code></td><td><code>other</code> (default <code>null</code>)</td></tr>
205+
<tr><td><code>dataFrameWhere(df, cond, {other})</code></td><td><code>true</code></td><td><code>other</code> (default <code>null</code>)</td></tr>
206+
<tr><td><code>dataFrameMask(df, cond, {other})</code></td><td><code>false</code></td><td><code>other</code> (default <code>null</code>)</td></tr>
207+
</table>
208+
209+
<h3>Condition types</h3>
210+
<table>
211+
<tr><th>Type</th><th>Series ops</th><th>DataFrame ops</th></tr>
212+
<tr><td>Boolean array</td><td>✅ positional</td><td></td></tr>
213+
<tr><td><code>Series&lt;boolean&gt;</code></td><td>✅ label-aligned</td><td></td></tr>
214+
<tr><td><code>DataFrame</code> (boolean)</td><td></td><td>✅ label-aligned</td></tr>
215+
<tr><td>Callable</td><td>✅ receives Series</td><td>✅ receives DataFrame</td></tr>
216+
</table>
217+
218+
<p><a href="index.html">← Back to tsb playground index</a></p>
219+
</body>
220+
</html>

src/index.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,3 +118,10 @@ export { cut, qcut } from "./stats/index.ts";
118118
export type { BinResult, CutOptions, QCutOptions } from "./stats/index.ts";
119119
export { rollingSem, rollingSkew, rollingKurt, rollingQuantile } from "./stats/index.ts";
120120
export type { WindowExtOptions, RollingQuantileOptions } from "./stats/index.ts";
121+
export { seriesWhere, seriesMask, dataFrameWhere, dataFrameMask } from "./stats/index.ts";
122+
export type {
123+
SeriesCond,
124+
DataFrameCond,
125+
SeriesWhereOptions,
126+
DataFrameWhereOptions,
127+
} from "./stats/index.ts";

src/stats/index.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,3 +43,10 @@ export { cut, qcut } from "./cut_qcut.ts";
4343
export type { BinResult, CutOptions, QCutOptions } from "./cut_qcut.ts";
4444
export { rollingSem, rollingSkew, rollingKurt, rollingQuantile } from "./window_extended.ts";
4545
export type { WindowExtOptions, RollingQuantileOptions } from "./window_extended.ts";
46+
export { seriesWhere, seriesMask, dataFrameWhere, dataFrameMask } from "./where_mask.ts";
47+
export type {
48+
SeriesCond,
49+
DataFrameCond,
50+
SeriesWhereOptions,
51+
DataFrameWhereOptions,
52+
} from "./where_mask.ts";

0 commit comments

Comments
 (0)