Skip to content

Commit b3ad0f3

Browse files
Iteration 69: Add nlargest/nsmallest — Series and DataFrame top-n selection
- src/stats/nlargest.ts: nlargestSeries, nsmallestSeries, nlargestDataFrame, nsmallestDataFrame - Mirror pandas Series.nlargest/nsmallest and DataFrame.nlargest/nsmallest - keep='first'/'last'/'all' tie-handling at selection boundary - NaN/null values excluded from result (same as pandas) - Multi-column DataFrame sorting with lexicographic comparison - 45+ unit tests + 5 property-based tests - Playground: nlargest.html (8 sections) Run: https://github.com/githubnext/tsessebe/actions/runs/24009034419 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent a8a0659 commit b3ad0f3

3 files changed

Lines changed: 1040 additions & 0 deletions

File tree

playground/nlargest.html

Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
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 — nlargest / nsmallest</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>nlargest / nsmallest</h1>
26+
<p class="subtitle">Return the <em>n</em> largest or smallest values — mirrors <code>pandas.Series.nlargest()</code>, <code>Series.nsmallest()</code>, <code>DataFrame.nlargest()</code>, <code>DataFrame.nsmallest()</code>.</p>
27+
28+
<section>
29+
<h2>1 — Series.nlargest basics</h2>
30+
<p><code>nlargestSeries(s, n)</code> returns a new Series containing the <em>n</em> largest values, sorted in descending order. NaN / null values are always excluded.</p>
31+
<pre><code id="ex1-code">import { Series, nlargestSeries } from "tsb";
32+
33+
const s = new Series({ data: [3, 1, 4, 1, 5, 9, 2, 6] });
34+
35+
// 3 largest values, sorted descending
36+
const top3 = nlargestSeries(s, 3);
37+
console.log([...top3.values]); // [9, 6, 5]
38+
console.log([...top3.index.values]); // original positions: [5, 7, 4]
39+
</code></pre>
40+
<div class="output" id="ex1-output">Loading…</div>
41+
</section>
42+
43+
<section>
44+
<h2>2 — Series.nsmallest basics</h2>
45+
<p><code>nsmallestSeries(s, n)</code> returns the <em>n</em> smallest values sorted in ascending order.</p>
46+
<pre><code id="ex2-code">import { Series, nsmallestSeries } from "tsb";
47+
48+
const s = new Series({ data: [3, 1, 4, 1, 5, 9, 2, 6] });
49+
50+
const bottom3 = nsmallestSeries(s, 3);
51+
console.log([...bottom3.values]); // [1, 1, 2]
52+
console.log([...bottom3.index.values]); // [1, 3, 6]
53+
</code></pre>
54+
<div class="output" id="ex2-output">Loading…</div>
55+
</section>
56+
57+
<section>
58+
<h2>3 — The <code>keep</code> parameter</h2>
59+
<p>When there are ties at the selection boundary, <code>keep</code> controls which ones survive:</p>
60+
<ul>
61+
<li><strong>first</strong> (default): keep the first occurrence in the original index order.</li>
62+
<li><strong>last</strong>: keep the last occurrence.</li>
63+
<li><strong>all</strong>: include every tie (may return more than <em>n</em> rows).</li>
64+
</ul>
65+
<pre><code id="ex3-code">import { Series, nlargestSeries } from "tsb";
66+
67+
const s = new Series({
68+
data: [1, 3, 3, 5],
69+
index: ["a", "b", "c", "d"],
70+
});
71+
72+
// n=2: top two are 5 (d) and one of the 3s
73+
nlargestSeries(s, 2, { keep: "first" }).values; // [5, 3] — keeps "b" (first 3)
74+
nlargestSeries(s, 2, { keep: "last" }).values; // [5, 3] — keeps "c" (last 3)
75+
76+
// keep="all" includes both 3s even though n=2
77+
const all = nlargestSeries(s, 2, { keep: "all" });
78+
console.log([...all.values]); // [5, 3, 3]
79+
console.log([...all.index.values]); // ["d", "b", "c"]
80+
</code></pre>
81+
<div class="output" id="ex3-output">Loading…</div>
82+
</section>
83+
84+
<section>
85+
<h2>4 — Labeled index preservation</h2>
86+
<p>The result preserves the original labels, not a reset 0-based index.</p>
87+
<pre><code id="ex4-code">import { Series, nlargestSeries, nsmallestSeries } from "tsb";
88+
89+
const prices = new Series({
90+
data: [42.5, 18.0, 93.1, 67.8, 55.4],
91+
index: ["AAPL", "META", "NVDA", "MSFT", "AMZN"],
92+
});
93+
94+
const topStocks = nlargestSeries(prices, 3);
95+
console.log([...topStocks.values]); // [93.1, 67.8, 55.4]
96+
console.log([...topStocks.index.values]); // ["NVDA", "MSFT", "AMZN"]
97+
98+
const cheapest = nsmallestSeries(prices, 2);
99+
console.log([...cheapest.values]); // [18.0, 42.5]
100+
console.log([...cheapest.index.values]); // ["META", "AAPL"]
101+
</code></pre>
102+
<div class="output" id="ex4-output">Loading…</div>
103+
</section>
104+
105+
<section>
106+
<h2>5 — NaN / null handling</h2>
107+
<p>Missing values are silently excluded from both the selection and the result.</p>
108+
<pre><code id="ex5-code">import { Series, nlargestSeries } from "tsb";
109+
110+
const s = new Series({ data: [3, NaN, 9, null, 1, NaN, 5] });
111+
const top3 = nlargestSeries(s, 3);
112+
console.log([...top3.values]); // [9, 5, 3] — NaN/null are gone
113+
console.log(top3.size); // 3
114+
</code></pre>
115+
<div class="output" id="ex5-output">Loading…</div>
116+
</section>
117+
118+
<section>
119+
<h2>6 — DataFrame.nlargest</h2>
120+
<p><code>nlargestDataFrame(df, n, { columns })</code> returns the <em>n</em> rows with the largest values in the given column(s), sorted descending. Multiple columns provide a lexicographic tie-breaker.</p>
121+
<pre><code id="ex6-code">import { DataFrame, nlargestDataFrame } from "tsb";
122+
123+
const df = DataFrame.fromColumns({
124+
name: ["Alice", "Bob", "Carol", "Dave", "Eve"],
125+
score: [88, 95, 72, 95, 80],
126+
age: [30, 25, 35, 28, 22],
127+
});
128+
129+
// Top 3 scorers
130+
const top3 = nlargestDataFrame(df, 3, { columns: "score" });
131+
console.log([...top3.col("name").values]); // ["Bob", "Dave", "Carol" or "Eve"]
132+
console.log([...top3.col("score").values]); // [95, 95, 88]
133+
134+
// Break ties on "score" using "age" (secondary sort descending)
135+
const top2ByAge = nlargestDataFrame(df, 2, { columns: ["score", "age"] });
136+
console.log([...top2ByAge.col("name").values]); // ["Bob", "Dave"] (both 95; Bob is 25 < Dave 28)
137+
</code></pre>
138+
<div class="output" id="ex6-output">Loading…</div>
139+
</section>
140+
141+
<section>
142+
<h2>7 — DataFrame.nsmallest</h2>
143+
<p><code>nsmallestDataFrame(df, n, { columns })</code> returns the rows with the smallest values, sorted ascending.</p>
144+
<pre><code id="ex7-code">import { DataFrame, nsmallestDataFrame } from "tsb";
145+
146+
const df = DataFrame.fromColumns({
147+
product: ["A", "B", "C", "D", "E"],
148+
price: [12.5, 9.9, 12.5, 7.0, 20.0],
149+
rating: [4.2, 3.8, 4.5, 4.1, 3.2],
150+
});
151+
152+
// 2 cheapest products
153+
const cheapest2 = nsmallestDataFrame(df, 2, { columns: "price" });
154+
console.log([...cheapest2.col("product").values]); // ["D", "B"]
155+
console.log([...cheapest2.col("price").values]); // [7.0, 9.9]
156+
</code></pre>
157+
<div class="output" id="ex7-output">Loading…</div>
158+
</section>
159+
160+
<section>
161+
<h2>8 — Edge cases</h2>
162+
<pre><code id="ex8-code">import { Series, nlargestSeries, nsmallestSeries } from "tsb";
163+
164+
// n > series length → return all (sorted)
165+
const s = new Series({ data: [2, 1, 3] });
166+
console.log([...nlargestSeries(s, 100).values]); // [3, 2, 1]
167+
console.log([...nsmallestSeries(s, 100).values]); // [1, 2, 3]
168+
169+
// n = 0 → empty
170+
console.log(nlargestSeries(s, 0).size); // 0
171+
172+
// all-NaN → empty
173+
const allNaN = new Series({ data: [NaN, NaN] });
174+
console.log(nlargestSeries(allNaN, 2).size); // 0
175+
176+
// string values — lexicographic order
177+
const words = new Series({ data: ["banana", "apple", "cherry"] });
178+
console.log([...nlargestSeries(words, 2).values]); // ["cherry", "banana"]
179+
console.log([...nsmallestSeries(words, 2).values]); // ["apple", "banana"]
180+
</code></pre>
181+
<div class="output" id="ex8-output">Loading…</div>
182+
</section>
183+
184+
<script>
185+
window.__tsb_examples__ = [
186+
{ id: "ex1", code: document.getElementById("ex1-code").textContent },
187+
{ id: "ex2", code: document.getElementById("ex2-code").textContent },
188+
{ id: "ex3", code: document.getElementById("ex3-code").textContent },
189+
{ id: "ex4", code: document.getElementById("ex4-code").textContent },
190+
{ id: "ex5", code: document.getElementById("ex5-code").textContent },
191+
{ id: "ex6", code: document.getElementById("ex6-code").textContent },
192+
{ id: "ex7", code: document.getElementById("ex7-code").textContent },
193+
{ id: "ex8", code: document.getElementById("ex8-code").textContent },
194+
];
195+
</script>
196+
</body>
197+
</html>

0 commit comments

Comments
 (0)