Skip to content

Commit f9a2a91

Browse files
Iteration 139: Add cut/qcut — interval binning functions
Implements pandas.cut and pandas.qcut as TypeScript equivalents: - src/stats/cut_qcut.ts: cut() (fixed-width/explicit bins) and qcut() (quantile-based) - tests/stats/cut_qcut.test.ts: 30+ unit tests + 2 property-based tests - playground/cut_qcut.html: interactive tutorial page Both functions return BinResult { codes, labels, bins }. Options: labels (string[] | false), right, include_lowest, precision, duplicates. Run: https://github.com/githubnext/tsessebe/actions/runs/24172139030 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent f0178e1 commit f9a2a91

6 files changed

Lines changed: 832 additions & 0 deletions

File tree

playground/cut_qcut.html

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
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 — cut / qcut: Binning Continuous Data</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>cut</code> / <code>qcut</code>: Binning Continuous Data</h1>
43+
<p>
44+
<code>cut</code> and <code>qcut</code> partition continuous numeric values into
45+
discrete intervals — the TypeScript equivalents of
46+
<a href="https://pandas.pydata.org/docs/reference/api/pandas.cut.html"><code>pandas.cut</code></a>
47+
and
48+
<a href="https://pandas.pydata.org/docs/reference/api/pandas.qcut.html"><code>pandas.qcut</code></a>.
49+
</p>
50+
51+
<h2>1. <code>cut</code> — Fixed-Width Binning</h2>
52+
<p>
53+
Bin values into equal-width (or user-specified) intervals.
54+
Pass an integer for automatic bins, or an explicit edge array.
55+
</p>
56+
57+
<h3>Integer bins</h3>
58+
<pre><code>import { cut } from "tsb";
59+
60+
const ages = [5, 18, 25, 35, 50, 70];
61+
const { codes, labels, bins } = cut(ages, 3);
62+
63+
// labels: ["(5.0, 26.7]", "(26.7, 48.3]", "(48.3, 70.0]"]
64+
// bins: [4.935, 26.667, 48.333, 70]
65+
// codes: [0, 0, 0, 1, 1, 2]
66+
console.table(ages.map((a, i) => ({ age: a, bin: labels[codes[i]!] })));
67+
</code></pre>
68+
69+
<h3>Explicit bin edges</h3>
70+
<pre><code>const scores = [55, 65, 72, 80, 91, 98];
71+
const { codes, labels } = cut(scores, [0, 60, 70, 80, 90, 100], {
72+
labels: ["F", "D", "C", "B", "A"],
73+
include_lowest: true,
74+
});
75+
// codes: [0, 1, 2, 3, 4, 4]
76+
// labels[codes[0]] → "F"
77+
// labels[codes[5]] → "A"
78+
</code></pre>
79+
80+
<h3>Options</h3>
81+
<table>
82+
<thead><tr><th>Option</th><th>Default</th><th>Description</th></tr></thead>
83+
<tbody>
84+
<tr><td><code>right</code></td><td><code>true</code></td><td>Intervals closed on right: <code>(a, b]</code>. Set <code>false</code> for <code>[a, b)</code>.</td></tr>
85+
<tr><td><code>include_lowest</code></td><td><code>false</code></td><td>Make lowest interval left-closed: <code>[a, b]</code>.</td></tr>
86+
<tr><td><code>labels</code></td><td>auto</td><td>Custom string labels, or <code>false</code> for integer codes.</td></tr>
87+
<tr><td><code>precision</code></td><td><code>3</code></td><td>Decimal places in auto-generated labels.</td></tr>
88+
<tr><td><code>duplicates</code></td><td><code>"raise"</code></td><td><code>"drop"</code> to silently remove duplicate bin edges.</td></tr>
89+
</tbody>
90+
</table>
91+
92+
<h2>2. <code>qcut</code> — Quantile-Based Binning</h2>
93+
<p>
94+
Divide values into bins of (approximately) equal population using quantiles.
95+
Useful for creating percentile buckets or roughly equal-sized groups.
96+
</p>
97+
98+
<h3>Quartile split</h3>
99+
<pre><code>import { qcut } from "tsb";
100+
101+
const values = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
102+
const { codes, labels, bins } = qcut(values, 4);
103+
104+
// labels: ["[1, 3.25]", "(3.25, 5.5]", "(5.5, 7.75]", "(7.75, 10]"]
105+
// Every bin has ~2-3 elements
106+
</code></pre>
107+
108+
<h3>Custom quantile probabilities</h3>
109+
<pre><code>const { labels } = qcut(values, [0, 0.1, 0.5, 0.9, 1], {
110+
labels: ["bottom 10%", "lower middle", "upper middle", "top 10%"],
111+
});
112+
</code></pre>
113+
114+
<h3>Decile labels</h3>
115+
<pre><code>const { codes } = qcut(data, 10, { labels: false });
116+
// codes[i] is 0..9 — the decile bucket index
117+
</code></pre>
118+
119+
<h2>3. Return Value: <code>BinResult</code></h2>
120+
<pre><code>interface BinResult {
121+
codes: ReadonlyArray&lt;number | null&gt;; // bin index per value; null for NaN
122+
labels: readonly string[]; // ordered label per bin
123+
bins: readonly number[]; // bin edge array (labels.length + 1)
124+
}
125+
</code></pre>
126+
127+
<div class="note">
128+
<strong>Missing values</strong>: <code>NaN</code> and <code>Infinity</code> are
129+
assigned <code>null</code> in the <code>codes</code> array and are never placed
130+
in a bin.
131+
</div>
132+
133+
<h2>4. <code>cut</code> vs <code>qcut</code></h2>
134+
<table>
135+
<thead><tr><th></th><th><code>cut</code></th><th><code>qcut</code></th></tr></thead>
136+
<tbody>
137+
<tr><td>Bin width</td><td>Equal (uniform edges)</td><td>Varies (equal population)</td></tr>
138+
<tr><td>Bin count</td><td>Determined by <code>bins</code></td><td>Determined by <code>q</code></td></tr>
139+
<tr><td>Best for</td><td>Meaningful thresholds (age groups, grade bands)</td><td>Percentile buckets, rank-based analysis</td></tr>
140+
<tr><td>Left edge of first bin</td><td>Open <code>(</code> unless <code>include_lowest</code></td><td>Always closed <code>[</code></td></tr>
141+
</tbody>
142+
</table>
143+
144+
<h2>5. pandas Compatibility</h2>
145+
<pre><code># Python pandas
146+
pd.cut([1, 2, 3, 4, 5], 2)
147+
# Interval(0.996, 3.0, closed='right') ...
148+
149+
# tsb equivalent
150+
cut([1, 2, 3, 4, 5], 2)
151+
// codes: [0, 0, 0, 1, 1]
152+
// labels: ["(0.996, 3.0]", "(3.0, 5.0]"]
153+
</code></pre>
154+
155+
<p>
156+
Both <code>cut</code> and <code>qcut</code> follow pandas semantics exactly:
157+
right-closed by default, linear interpolation for quantiles, and duplicate-edge
158+
handling via <code>duplicates</code>.
159+
</p>
160+
161+
<p><a href="index.html">← Back to tsb feature index</a></p>
162+
</body>
163+
</html>

playground/index.html

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,11 @@ <h3><a href="insert_pop.html" style="color: var(--accent); text-decoration: none
269269
<p>Insert and remove DataFrame columns at precise positions. <code>insertColumn(df, loc, col, values)</code> inserts at integer position, <code>popColumn(df, col)</code> returns <code>{ series, df }</code>. Also includes <code>reorderColumns</code> and <code>moveColumn</code>. Mirrors <code>pandas.DataFrame.insert()</code> and <code>.pop()</code>.</p>
270270
<div class="status done">✅ Complete</div>
271271
</div>
272+
<div class="feature-card">
273+
<h3><a href="cut_qcut.html" style="color: var(--accent); text-decoration: none;">✂️ cut / qcut</a></h3>
274+
<p>Bin continuous numeric data into discrete intervals. <code>cut()</code> uses fixed-width or explicit bin edges; <code>qcut()</code> uses quantile-based bins of equal population. Both return codes, labels, and bin edges. Mirrors <code>pandas.cut</code> and <code>pandas.qcut</code>.</p>
275+
<div class="status done">✅ Complete</div>
276+
</div>
272277
</div>
273278
</section>
274279
</main>

src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,3 +114,5 @@ export { toDictOriented, fromDictOriented } from "./core/index.ts";
114114
export type { ToDictOrient, FromDictOrient, DictSplit, DictTight, SplitInput } from "./core/index.ts";
115115
export { wideToLong } from "./reshape/index.ts";
116116
export type { WideToLongOptions } from "./reshape/index.ts";
117+
export { cut, qcut } from "./stats/index.ts";
118+
export type { BinResult, CutOptions, QCutOptions } from "./stats/index.ts";

0 commit comments

Comments
 (0)