Skip to content

Commit a8a0659

Browse files
Iteration 68: Add rank() — rankSeries and rankDataFrame
Add src/stats/rank.ts implementing pandas.Series.rank() / DataFrame.rank(): - rankSeries(series, options): assign numerical ranks to Series values - rankDataFrame(df, options): rank columns (axis=0) or rows (axis=1) - Tie methods: average (default), min, max, first, dense - NaN handling: keep (→ NaN), top (lowest ranks), bottom (highest ranks) - Percentage rank: pct=true divides by nValid (keep) or n (top/bottom) Tests: 40+ unit tests + 6 property-based tests (fast-check). Playground: playground/rank.html (8 interactive sections). Metric: 23 → 24 pandas_features_ported. Run: https://github.com/githubnext/tsessebe/actions/runs/24008535770 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 62962f8 commit a8a0659

3 files changed

Lines changed: 939 additions & 0 deletions

File tree

playground/rank.html

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
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 — rank</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>rank</h1>
26+
<p class="subtitle">Assign numerical ranks to values — mirrors <code>pandas.Series.rank()</code> / <code>pandas.DataFrame.rank()</code>.</p>
27+
28+
<section>
29+
<h2>1 — Basic ranking</h2>
30+
<p>By default, <code>rankSeries</code> uses <code>method="average"</code> (tied values share the average of their ranks) and <code>ascending=true</code> (smallest value gets rank 1).</p>
31+
<pre><code id="ex1-code">import { Series, rankSeries } from "tsb";
32+
33+
const s = new Series({ data: [3, 1, 4, 1, 5] });
34+
const r = rankSeries(s);
35+
console.log([...r.values]); // [3, 1.5, 4, 1.5, 5]
36+
// 1 appears twice (positions 1 and 3) → average rank (1+2)/2 = 1.5
37+
</code></pre>
38+
<div class="output" id="ex1-output">Loading…</div>
39+
</section>
40+
41+
<section>
42+
<h2>2 — Tie-breaking methods</h2>
43+
<p>Five methods mirror pandas:</p>
44+
<ul>
45+
<li><strong>average</strong> (default): tied values share the average rank</li>
46+
<li><strong>min</strong>: tied values get the lowest rank</li>
47+
<li><strong>max</strong>: tied values get the highest rank</li>
48+
<li><strong>first</strong>: breaks ties by order of appearance</li>
49+
<li><strong>dense</strong>: like min but no rank gaps between distinct values</li>
50+
</ul>
51+
<pre><code id="ex2-code">import { Series, rankSeries } from "tsb";
52+
53+
const s = new Series({ data: [3, 1, 4, 1, 5] });
54+
55+
console.log([...rankSeries(s, { method: "average" }).values]); // [3, 1.5, 4, 1.5, 5]
56+
console.log([...rankSeries(s, { method: "min" }).values]); // [3, 1, 4, 1, 5]
57+
console.log([...rankSeries(s, { method: "max" }).values]); // [3, 2, 4, 2, 5]
58+
console.log([...rankSeries(s, { method: "first" }).values]); // [3, 1, 4, 2, 5]
59+
console.log([...rankSeries(s, { method: "dense" }).values]); // [3, 1, 4, 1, 5]
60+
</code></pre>
61+
<div class="output" id="ex2-output">Loading…</div>
62+
</section>
63+
64+
<section>
65+
<h2>3 — Descending rank</h2>
66+
<p>Set <code>ascending: false</code> to give rank 1 to the largest value.</p>
67+
<pre><code id="ex3-code">import { Series, rankSeries } from "tsb";
68+
69+
const scores = new Series({ data: [85, 92, 78, 95, 88], index: ["Alice","Bob","Charlie","Dana","Eve"] });
70+
const rank = rankSeries(scores, { ascending: false });
71+
console.log([...rank.values]); // [4, 2, 5, 1, 3]
72+
// Dana (95) gets rank 1, Bob (92) gets rank 2, etc.
73+
</code></pre>
74+
<div class="output" id="ex3-output">Loading…</div>
75+
</section>
76+
77+
<section>
78+
<h2>4 — Handling NaN/null values</h2>
79+
<p>Three strategies mirror pandas <code>na_option</code>:</p>
80+
<ul>
81+
<li><strong>keep</strong> (default): missing values → NaN in result</li>
82+
<li><strong>top</strong>: missing values get the lowest ranks (come first)</li>
83+
<li><strong>bottom</strong>: missing values get the highest ranks (come last)</li>
84+
</ul>
85+
<pre><code id="ex4-code">import { Series, rankSeries } from "tsb";
86+
87+
const s = new Series({ data: [3, null, 1, null, 2] });
88+
89+
const keep = rankSeries(s, { naOption: "keep" });
90+
const top = rankSeries(s, { naOption: "top" });
91+
const bottom = rankSeries(s, { naOption: "bottom" });
92+
93+
console.log([...keep.values]); // [3, NaN, 1, NaN, 2]
94+
console.log([...top.values]); // [5, 1, 3, 2, 4] — nulls rank first
95+
console.log([...bottom.values]); // [3, 4, 1, 5, 2] — nulls rank last
96+
</code></pre>
97+
<div class="output" id="ex4-output">Loading…</div>
98+
</section>
99+
100+
<section>
101+
<h2>5 — Percentage rank (pct)</h2>
102+
<p><code>pct: true</code> returns fractional ranks in (0, 1].</p>
103+
<pre><code id="ex5-code">import { Series, rankSeries } from "tsb";
104+
105+
const s = new Series({ data: [10, 20, 30, 40, 50] });
106+
const pct = rankSeries(s, { pct: true });
107+
console.log([...pct.values]); // [0.2, 0.4, 0.6, 0.8, 1.0]
108+
</code></pre>
109+
<div class="output" id="ex5-output">Loading…</div>
110+
</section>
111+
112+
<section>
113+
<h2>6 — DataFrame rank by column (axis=0)</h2>
114+
<p><code>rankDataFrame</code> ranks each column independently by default.</p>
115+
<pre><code id="ex6-code">import { DataFrame, rankDataFrame } from "tsb";
116+
117+
const df = DataFrame.fromColumns({
118+
score: [85, 92, 78],
119+
time: [12, 8, 15],
120+
}, { index: ["Alice", "Bob", "Charlie"] });
121+
122+
const ranked = rankDataFrame(df);
123+
console.log([...ranked.col("score").values]); // [2, 3, 1]
124+
console.log([...ranked.col("time").values]); // [2, 1, 3] — lower time = lower rank
125+
</code></pre>
126+
<div class="output" id="ex6-output">Loading…</div>
127+
</section>
128+
129+
<section>
130+
<h2>7 — DataFrame rank by row (axis=1)</h2>
131+
<p>Set <code>axis: 1</code> to rank each row's values relative to each other.</p>
132+
<pre><code id="ex7-code">import { DataFrame, rankDataFrame } from "tsb";
133+
134+
const df = DataFrame.fromColumns({
135+
math: [90, 70, 85],
136+
english: [80, 95, 75],
137+
science: [85, 80, 90],
138+
});
139+
140+
// For each student, rank their three subject scores
141+
const ranked = rankDataFrame(df, { axis: 1 });
142+
console.log([...ranked.col("math").values]); // [3, 1, 2]
143+
console.log([...ranked.col("english").values]); // [1, 3, 1]
144+
console.log([...ranked.col("science").values]); // [2, 2, 3]
145+
</code></pre>
146+
<div class="output" id="ex7-output">Loading…</div>
147+
</section>
148+
149+
<section>
150+
<h2>8 — Dense rank for competition rankings</h2>
151+
<p>Dense rank is useful when you want no gaps — e.g., "1st, 2nd, 2nd, 3rd" instead of "1st, 2nd, 2nd, 4th".</p>
152+
<pre><code id="ex8-code">import { Series, rankSeries } from "tsb";
153+
154+
const points = new Series({
155+
data: [100, 85, 85, 72, 72, 60],
156+
index: ["A", "B", "C", "D", "E", "F"],
157+
});
158+
159+
const denseRank = rankSeries(points, { method: "dense", ascending: false });
160+
console.log([...denseRank.values]); // [1, 2, 2, 3, 3, 4]
161+
// Tied players share the same position; no position is skipped
162+
</code></pre>
163+
<div class="output" id="ex8-output">Loading…</div>
164+
</section>
165+
166+
<script>
167+
window.__tsb_examples__ = [
168+
{ id: "ex1", code: document.getElementById("ex1-code").textContent },
169+
{ id: "ex2", code: document.getElementById("ex2-code").textContent },
170+
{ id: "ex3", code: document.getElementById("ex3-code").textContent },
171+
{ id: "ex4", code: document.getElementById("ex4-code").textContent },
172+
{ id: "ex5", code: document.getElementById("ex5-code").textContent },
173+
{ id: "ex6", code: document.getElementById("ex6-code").textContent },
174+
{ id: "ex7", code: document.getElementById("ex7-code").textContent },
175+
{ id: "ex8", code: document.getElementById("ex8-code").textContent },
176+
];
177+
</script>
178+
</body>
179+
</html>

0 commit comments

Comments
 (0)