Tutorial: rank a leaderboard
In this tutorial you build the leaderboard of an online game. Players with more points rank higher. Players with equal points share a rank, and the faster finish comes first in the table. At the end you run the same code on a million players.
It takes about 10 minutes. You need Python with NumPy and Kwker installed (Installation).
Step 1: the scores
Each player has a name, a number of points and a finish time in seconds. Keep them as three NumPy arrays of the same
length: row i of each array is player i.
import numpy as np
import kwker
names = np.array(["ana", "ben", "cho", "dev", "eli", "fay", "gus", "hal"])
points = np.array([820, 950, 820, 990, 640, 950, 820, 700])
seconds = np.array([312, 298, 287, 301, 340, 305, 290, 333])
print(len(names), "players")
8 players
Step 2: ranks with ties
rank gives every player a rank. With method="min", equal points share the best rank of their group, and the next
rank skips ahead: two players at rank 2 are followed by rank 4. This is the ranking most competitions use (and SQL's
RANK()). descending=True gives rank 1 to the most points.
ranks = kwker.rank(points, method="min", descending=True)
for name, p, r in zip(names, points, ranks):
print(f"{name}: {p} points, rank {r}")
ana: 820 points, rank 4 ben: 950 points, rank 2 cho: 820 points, rank 4 dev: 990 points, rank 1 eli: 640 points, rank 8 fay: 950 points, rank 2 gus: 820 points, rank 4 hal: 700 points, rank 7
method="dense" would not skip: the players after the two at rank 2 would get rank 3.
Step 3: the sorted table
To print the table you need the order of the rows. lexsort sorts by several columns: the first column decides,
and the next one breaks its ties. Here that is points (most first) and then seconds (fastest first). The result is a
list of row numbers.
order = kwker.lexsort([points, seconds], descending=[True, False])
print(order)
for row in order:
print(f"{ranks[row]:>2} {names[row]:<4} {points[row]:>4} {seconds[row]:>4}s")
[3 1 5 2 6 0 7 4] 1 dev 990 301s 2 ben 950 298s 2 fay 950 305s 4 cho 820 287s 4 gus 820 290s 4 ana 820 312s 7 hal 700 333s 8 eli 640 340s
Players with equal points and equal seconds would keep their input order: lexsort is stable.
Step 4: only the top three
A front page shows the top three, not the whole table. lex_top_k finds the first rows of the same order without
sorting every row, which matters when the table is long.
top = kwker.lex_top_k([points, seconds], 3, descending=[True, False])
print([str(names[row]) for row in top])
['dev', 'ben', 'fay']
Step 5: a million players
Now make a leaderboard of a million players with random points and times, and run the same three calls. To check the
result, compare it with NumPy: numpy.lexsort takes its keys in the reverse order (the last key decides first), and it
has no descending flag, so negate the points.
rng = np.random.default_rng(7)
n = 1_000_000
points = rng.integers(0, 5000, n)
seconds = rng.integers(60, 3600, n)
ranks = kwker.rank(points, method="min", descending=True)
order = kwker.lexsort([points, seconds], descending=[True, False])
top = kwker.lex_top_k([points, seconds], 10, descending=[True, False])
expected = np.lexsort((seconds, -points))
print(np.array_equal(order, expected))
print(np.array_equal(top, expected[:10]))
print(ranks[order[0]], ranks[order[-1]])
True True 1 999800
The order matches NumPy row for row, the first player has rank 1, and the last has the rank after everyone with more points.
What you built
- Competition ranks with
rank(method="min"), where ties share a rank. - A sorted table with a tiebreak, with
lexsort. - The top of the table without a full sort, with
lex_top_k.
Next steps
- Order and ranking: every rank method, percent ranks, and sorting by several columns.
- Top-k and selection: top-k of one column, per group, and with a mask.
- Tutorial: top-k recommendations: pick the best items for every user at once.