Skip to content

Commit 0c05e67

Browse files
Iteration 150: categorical_ops — catFromCodes, set ops, catSortByFreq, catFreqTable, catCrossTab, catRecode
Run: https://github.com/githubnext/tsessebe/actions/runs/24212535793
1 parent b500e3b commit 0c05e67

6 files changed

Lines changed: 1338 additions & 0 deletions

File tree

playground/categorical_ops.html

Lines changed: 338 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,338 @@
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 — Categorical Ops</title>
7+
<style>
8+
:root {
9+
--bg: #0d1117;
10+
--surface: #161b22;
11+
--border: #30363d;
12+
--text: #e6edf3;
13+
--accent: #58a6ff;
14+
--green: #3fb950;
15+
--orange: #d29922;
16+
--red: #f85149;
17+
--font-mono: "Cascadia Code", "Fira Code", "JetBrains Mono", monospace;
18+
}
19+
* { box-sizing: border-box; margin: 0; padding: 0; }
20+
body {
21+
background: var(--bg);
22+
color: var(--text);
23+
font-family: system-ui, -apple-system, sans-serif;
24+
line-height: 1.6;
25+
padding: 2rem;
26+
max-width: 900px;
27+
margin: 0 auto;
28+
}
29+
a { color: var(--accent); }
30+
h1 { color: var(--accent); margin-bottom: 0.5rem; }
31+
h2 { margin-top: 0; margin-bottom: 0.5rem; font-size: 1.25rem; }
32+
p { color: #8b949e; margin-bottom: 1rem; }
33+
code {
34+
font-family: var(--font-mono);
35+
font-size: 0.875em;
36+
background: var(--surface);
37+
border: 1px solid var(--border);
38+
border-radius: 0.3rem;
39+
padding: 0.1rem 0.4rem;
40+
}
41+
.back { margin-bottom: 2rem; display: inline-block; }
42+
#playground-loading {
43+
position: fixed;
44+
inset: 0;
45+
background: rgba(13, 17, 23, 0.92);
46+
display: flex;
47+
flex-direction: column;
48+
align-items: center;
49+
justify-content: center;
50+
z-index: 1000;
51+
gap: 1rem;
52+
}
53+
.spinner {
54+
width: 40px; height: 40px;
55+
border: 3px solid var(--border);
56+
border-top-color: var(--accent);
57+
border-radius: 50%;
58+
animation: spin 0.8s linear infinite;
59+
}
60+
@keyframes spin { to { transform: rotate(360deg); } }
61+
.card {
62+
background: var(--surface);
63+
border: 1px solid var(--border);
64+
border-radius: 0.5rem;
65+
padding: 1.5rem;
66+
margin-bottom: 1.5rem;
67+
}
68+
textarea {
69+
width: 100%;
70+
min-height: 140px;
71+
font-family: var(--font-mono);
72+
font-size: 0.85rem;
73+
background: var(--bg);
74+
color: var(--text);
75+
border: 1px solid var(--border);
76+
border-radius: 0.35rem;
77+
padding: 0.75rem;
78+
resize: vertical;
79+
}
80+
button {
81+
background: var(--accent);
82+
color: #0d1117;
83+
border: none;
84+
border-radius: 0.35rem;
85+
padding: 0.5rem 1.25rem;
86+
font-weight: 600;
87+
cursor: pointer;
88+
margin-top: 0.75rem;
89+
}
90+
button:hover { opacity: 0.85; }
91+
.output {
92+
margin-top: 0.75rem;
93+
font-family: var(--font-mono);
94+
font-size: 0.85rem;
95+
white-space: pre-wrap;
96+
background: var(--bg);
97+
border: 1px solid var(--border);
98+
border-radius: 0.35rem;
99+
padding: 0.75rem;
100+
min-height: 3rem;
101+
}
102+
.output.error { color: var(--red); }
103+
.output.ok { color: var(--green); }
104+
table { border-collapse: collapse; font-family: var(--font-mono); font-size: 0.85rem; }
105+
th, td { border: 1px solid var(--border); padding: 0.3rem 0.75rem; }
106+
th { background: var(--surface); color: var(--accent); }
107+
</style>
108+
</head>
109+
<body>
110+
<div id="playground-loading">
111+
<div class="spinner"></div>
112+
<span>Loading tsb runtime…</span>
113+
</div>
114+
115+
<a class="back" href="index.html">← back to index</a>
116+
<h1>🏷️ Categorical Ops</h1>
117+
<p>
118+
Standalone categorical utility functions that complement the <code>Series.cat</code> accessor.
119+
Mirrors <code>pd.Categorical.from_codes</code>, set operations on categories, frequency helpers,
120+
and cross-tabulation.
121+
</p>
122+
123+
<!-- ── catFromCodes ─────────────────────────────────────────────────── -->
124+
<div class="card">
125+
<h2><code>catFromCodes(codes, categories, opts?)</code></h2>
126+
<p>
127+
Construct a categorical Series from integer codes (0-based) and a categories array.
128+
Code <code>-1</code> maps to <code>null</code> (missing). Mirrors
129+
<code>pd.Categorical.from_codes</code>.
130+
</p>
131+
<textarea id="code-from-codes">const { catFromCodes } = tsb;
132+
133+
// Build a categorical from codes + category list
134+
const s = catFromCodes([0, 2, 1, -1, 0], ["red", "green", "blue"]);
135+
console.log("values:", s.toArray());
136+
console.log("categories:", s.cat.categories.values);
137+
console.log("ordered:", s.cat.ordered);
138+
139+
// With ordered=true
140+
const grade = catFromCodes([2, 0, 1, 2], ["F", "C", "A"], { ordered: true, name: "grade" });
141+
console.log("grade values:", grade.toArray());
142+
console.log("grade ordered:", grade.cat.ordered);
143+
console.log("grade codes back:", grade.cat.codes.toArray());
144+
</textarea>
145+
<button onclick="run('code-from-codes','out-from-codes')">▶ Run</button>
146+
<div class="output" id="out-from-codes"></div>
147+
</div>
148+
149+
<!-- ── Category set operations ──────────────────────────────────────── -->
150+
<div class="card">
151+
<h2>Category set operations</h2>
152+
<p>
153+
<code>catUnionCategories</code>, <code>catIntersectCategories</code>,
154+
<code>catDiffCategories</code>, and <code>catEqualCategories</code> let you
155+
combine or compare the category sets of two Series.
156+
</p>
157+
<textarea id="code-set-ops">const { Series, catUnionCategories, catIntersectCategories, catDiffCategories, catEqualCategories } = tsb;
158+
159+
const a = new Series({ data: ["x", "y", "z"] }).cat.setCategories(["x", "y", "z"]);
160+
const b = new Series({ data: ["y", "z", "w"] }).cat.setCategories(["y", "z", "w"]);
161+
162+
const union = catUnionCategories(a, b);
163+
console.log("union cats:", union.cat.categories.values); // x y z w
164+
165+
const inter = catIntersectCategories(a, b);
166+
console.log("intersect cats:", inter.cat.categories.values); // y z
167+
console.log("intersect vals:", inter.toArray()); // null y z
168+
169+
const diff = catDiffCategories(a, b);
170+
console.log("diff cats:", diff.cat.categories.values); // x
171+
console.log("diff vals:", diff.toArray()); // x null null
172+
173+
console.log("equal?", catEqualCategories(a, b)); // false
174+
const c = new Series({ data: [] }).cat.setCategories(["z", "x", "y"]);
175+
console.log("equal (diff order)?", catEqualCategories(a, c)); // true
176+
</textarea>
177+
<button onclick="run('code-set-ops','out-set-ops')">▶ Run</button>
178+
<div class="output" id="out-set-ops"></div>
179+
</div>
180+
181+
<!-- ── catSortByFreq ────────────────────────────────────────────────── -->
182+
<div class="card">
183+
<h2><code>catSortByFreq(series, opts?)</code></h2>
184+
<p>
185+
Reorder categories by their frequency in the data (most frequent first by default).
186+
Mirrors <code>s.cat.reorder_categories(s.value_counts().index)</code>.
187+
</p>
188+
<textarea id="code-sort-freq">const { Series, catSortByFreq } = tsb;
189+
190+
const s = new Series({ data: ["b", "a", "b", "c", "b", "a"] })
191+
.cat.setCategories(["a", "b", "c"]);
192+
193+
const sorted = catSortByFreq(s);
194+
console.log("categories (most frequent first):", sorted.cat.categories.values);
195+
// b=3, a=2, c=1 → ["b", "a", "c"]
196+
197+
const asc = catSortByFreq(s, { ascending: true });
198+
console.log("categories (rarest first):", asc.cat.categories.values);
199+
// → ["c", "a", "b"]
200+
</textarea>
201+
<button onclick="run('code-sort-freq','out-sort-freq')">▶ Run</button>
202+
<div class="output" id="out-sort-freq"></div>
203+
</div>
204+
205+
<!-- ── catToOrdinal ─────────────────────────────────────────────────── -->
206+
<div class="card">
207+
<h2><code>catToOrdinal(series, order)</code></h2>
208+
<p>
209+
Create an ordered categorical from a Series using <code>order</code> to define both the
210+
category set and their rank. Values not in <code>order</code> become <code>null</code>.
211+
</p>
212+
<textarea id="code-to-ordinal">const { Series, catToOrdinal } = tsb;
213+
214+
const s = new Series({ data: ["med", "low", "high", "med", "extreme"] });
215+
const ord = catToOrdinal(s, ["low", "med", "high"]);
216+
217+
console.log("values:", ord.toArray()); // med low high med null
218+
console.log("ordered:", ord.cat.ordered); // true
219+
console.log("categories:", ord.cat.categories.values); // low med high
220+
</textarea>
221+
<button onclick="run('code-to-ordinal','out-to-ordinal')">▶ Run</button>
222+
<div class="output" id="out-to-ordinal"></div>
223+
</div>
224+
225+
<!-- ── catFreqTable ─────────────────────────────────────────────────── -->
226+
<div class="card">
227+
<h2><code>catFreqTable(series)</code></h2>
228+
<p>
229+
Return a plain <code>Record&lt;string, number&gt;</code> of counts per category.
230+
Zero-frequency categories are included.
231+
</p>
232+
<textarea id="code-freq-table">const { Series, catFreqTable } = tsb;
233+
234+
const s = new Series({ data: ["b", "a", "b", null] })
235+
.cat.setCategories(["a", "b", "c"]);
236+
237+
const table = catFreqTable(s);
238+
console.log("frequency table:", table);
239+
// { a: 1, b: 2, c: 0 }
240+
241+
// Sum equals non-null count
242+
const total = Object.values(table).reduce((sum, n) => sum + n, 0);
243+
console.log("total (non-null):", total); // 3
244+
</textarea>
245+
<button onclick="run('code-freq-table','out-freq-table')">▶ Run</button>
246+
<div class="output" id="out-freq-table"></div>
247+
</div>
248+
249+
<!-- ── catCrossTab ──────────────────────────────────────────────────── -->
250+
<div class="card">
251+
<h2><code>catCrossTab(a, b, opts?)</code></h2>
252+
<p>
253+
Cross-tabulation of two categorical Series. Rows = <code>a</code>'s categories,
254+
columns = <code>b</code>'s categories, cells = co-occurrence counts.
255+
Supports margins and normalization.
256+
</p>
257+
<textarea id="code-crosstab">const { Series, catCrossTab } = tsb;
258+
259+
const gender = new Series({ data: ["M","F","M","F","M","F","M"] })
260+
.cat.setCategories(["M","F"]);
261+
const response = new Series({ data: ["Yes","No","Yes","Yes","No","Yes","Yes"] })
262+
.cat.setCategories(["Yes","No"]);
263+
264+
const ct = catCrossTab(gender, response);
265+
console.log("columns:", ct.columns.values);
266+
console.log("index:", [...ct.index.values]);
267+
console.log("Yes col:", ct.get("Yes").toArray());
268+
console.log("No col:", ct.get("No").toArray());
269+
270+
// With margins
271+
const ctm = catCrossTab(gender, response, { margins: true });
272+
console.log("\nWith margins:");
273+
console.log("columns:", ctm.columns.values);
274+
console.log("All col:", ctm.get("All").toArray());
275+
276+
// Normalized
277+
const ctn = catCrossTab(gender, response, { normalize: true });
278+
console.log("\nNormalized Yes col:", ctn.get("Yes").toArray());
279+
</textarea>
280+
<button onclick="run('code-crosstab','out-crosstab')">▶ Run</button>
281+
<div class="output" id="out-crosstab"></div>
282+
</div>
283+
284+
<!-- ── catRecode ────────────────────────────────────────────────────── -->
285+
<div class="card">
286+
<h2><code>catRecode(series, mapping)</code></h2>
287+
<p>
288+
Rename categories via an object map or a transform function. Unmapped categories
289+
are left unchanged.
290+
</p>
291+
<textarea id="code-recode">const { Series, catRecode } = tsb;
292+
293+
const s = new Series({ data: ["a", "b", "c"] }).cat.setCategories(["a", "b", "c"]);
294+
295+
// Object map (partial)
296+
const r1 = catRecode(s, { a: "alpha", b: "beta" });
297+
console.log("via map:", r1.cat.categories.values); // alpha beta c
298+
console.log("values:", r1.toArray()); // alpha beta c
299+
300+
// Transform function
301+
const r2 = catRecode(s, (x) => x.toUpperCase());
302+
console.log("via fn:", r2.cat.categories.values); // A B C
303+
console.log("values:", r2.toArray()); // A B C
304+
</textarea>
305+
<button onclick="run('code-recode','out-recode')">▶ Run</button>
306+
<div class="output" id="out-recode"></div>
307+
</div>
308+
309+
<script src="playground-runtime.js"></script>
310+
<script>
311+
async function run(codeId, outId) {
312+
const code = document.getElementById(codeId).value;
313+
const out = document.getElementById(outId);
314+
out.className = 'output';
315+
out.textContent = '…';
316+
try {
317+
const lines = [];
318+
const tsb = await getTsb();
319+
const origLog = console.log;
320+
console.log = (...args) => lines.push(args.map(v =>
321+
typeof v === 'object' && v !== null ? JSON.stringify(v) : String(v)
322+
).join(' '));
323+
try {
324+
const fn = new Function('tsb', code);
325+
await fn(tsb);
326+
} finally {
327+
console.log = origLog;
328+
}
329+
out.textContent = lines.join('\n') || '(no output)';
330+
out.classList.add('ok');
331+
} catch (e) {
332+
out.textContent = String(e);
333+
out.classList.add('error');
334+
}
335+
}
336+
</script>
337+
</body>
338+
</html>

playground/index.html

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -320,6 +320,13 @@ <h3><a href="numeric_extended.html" style="color: var(--accent); text-decoration
320320
<div class="status done">✅ Complete</div>
321321
</div>
322322
</div>
323+
<div class="feature-card done">
324+
<div class="feature-content">
325+
<h3><a href="categorical_ops.html" style="color: var(--accent); text-decoration: none;">🏷️ categorical_ops — Categorical Utilities</a></h3>
326+
<p>Standalone categorical helpers: <code>catFromCodes</code> (from integer codes), set operations (<code>catUnionCategories</code>, <code>catIntersectCategories</code>, <code>catDiffCategories</code>, <code>catEqualCategories</code>), <code>catSortByFreq</code>, <code>catToOrdinal</code>, <code>catFreqTable</code>, <code>catCrossTab</code>, <code>catRecode</code>.</p>
327+
<div class="status done">✅ Complete</div>
328+
</div>
329+
</div>
323330
</section>
324331
</main>
325332

src/index.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,3 +238,20 @@ export type {
238238
MinMaxOptions,
239239
CvOptions,
240240
} from "./stats/index.ts";
241+
export {
242+
catFromCodes,
243+
catUnionCategories,
244+
catIntersectCategories,
245+
catDiffCategories,
246+
catEqualCategories,
247+
catSortByFreq,
248+
catToOrdinal,
249+
catFreqTable,
250+
catCrossTab,
251+
catRecode,
252+
} from "./stats/index.ts";
253+
export type {
254+
CatFromCodesOptions,
255+
CatSortByFreqOptions,
256+
CatCrossTabOptions,
257+
} from "./stats/index.ts";

0 commit comments

Comments
 (0)