|
| 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