Kwker

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.

PythonRuns on your machine.
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")
Output
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.

PythonRuns on your machine.
ranks = kwker.rank(points, method="min", descending=True)
for name, p, r in zip(names, points, ranks):
    print(f"{name}: {p} points, rank {r}")
Output
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.

PythonRuns on your machine.
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")
Output
[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.

PythonRuns on your machine.
top = kwker.lex_top_k([points, seconds], 3, descending=[True, False])
print([str(names[row]) for row in top])
Output
['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.

PythonRuns on your machine.
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]])
Output
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

Next steps