Kwker

Order and ranking

Sometimes you need the order of your data rather than the sorted data itself: to reorder several columns the same way, to rank players, or to sort a table by more than one column. This page covers argsort, rank and the multi-column calls.

The order of an array: argsort

argsort(a) returns the positions that would sort a. The first entry is the position of the smallest value, the next one the position of the second smallest, and so on. a is not changed.

import numpy as np
import kwker

price = np.array([4.99, 1.25, 9.50, 2.75])
order = kwker.argsort(price)
print(order)
print(price[order])
[1 3 0 2]
[1.25 2.75 4.99 9.5 ]

argsort is stable: equal values keep their original order. That matters as soon as you use the order for other data.

Reorder several columns together

Sort one array and apply the same order to the others. Here, a table of people is sorted by age. Ben and Dara are both 27 and stay in their original order.

import numpy as np
import kwker

name = np.array(["Ana", "Ben", "Chen", "Dara", "Eli"])
age = np.array([34, 27, 41, 27, 30])
city = np.array(["Oslo", "Lima", "Kyiv", "Pune", "Rome"])
order = kwker.argsort(age)
for n, a, c in zip(name[order], age[order], city[order]):
    print(n, a, c)
Ben 27 Lima
Dara 27 Pune
Eli 30 Rome
Ana 34 Oslo
Chen 41 Kyiv

Ask for a descending order to get the largest first. Equal values still keep their original order.

Ranks

rank(a) gives each value its rank, starting at 1. Equal values need a rule, and the method chooses it:

method Equal values get Same as
"average" (Python's default) the mean of their ranks scipy.stats.rankdata
"min" the lowest of their ranks SQL RANK()
"max" the highest of their ranks
"dense" the same rank, with no gaps after SQL DENSE_RANK()
"ordinal" different ranks, in their original order SQL ROW_NUMBER()

In Rust the methods are RankTies::Min, Max, Dense and Ordinal (and rank_average); in C the ties argument 0 (ordinal), 1 (min), 2 (max) or 3 (dense), and kwker_<t>_rank_f64 for the average; in C++ kwker::Ties and rank_average.

import numpy as np
import kwker

points = np.array([10, 20, 20, 30])
for method in ["average", "min", "max", "dense", "ordinal"]:
    print(f"{method:8}", kwker.rank(points, method=method))
average  [1.  2.5 2.5 4. ]
min      [1 2 2 4]
max      [1 3 3 4]
dense    [1 2 2 3]
ordinal  [1 2 3 4]

percent_rank(a) returns where each value stands between 0 (the lowest) and 1 (the highest), as SQL's PERCENT_RANK() does.

import numpy as np
import kwker

print(kwker.percent_rank(np.array([10, 20, 20, 30])))
[0.         0.33333333 0.33333333 1.        ]

Sort by several columns

lexsort(columns) orders rows by the first column, then by the second where the first is equal, and so on, like SQL ORDER BY a, b. Each column can have its own direction.

import numpy as np
import kwker

team = np.array([2, 1, 2, 1, 2])
points = np.array([7, 9, 9, 4, 7])
order = kwker.lexsort([team, points], descending=[False, True])   # ORDER BY team, points DESC
print(order)
print(team[order], points[order])
[1 3 2 0 4]
[1 1 2 2 2] [9 4 9 7 7]

The first column decides first. NumPy's numpy.lexsort takes the columns in the opposite order, with the last column deciding first.

lex_top_k(columns, k) returns the first k rows of that order without ordering every row, like SQL ORDER BY ... LIMIT k.

import numpy as np
import kwker

team = np.array([2, 1, 2, 1, 2])
points = np.array([7, 9, 9, 4, 7])
print(kwker.lex_top_k([team, points], 2, descending=[False, True]))
[1 3]

Reorder records in place

permute_in_place(records, order) reorders an array of records (structs) by an order from argsort, without making a copy of the array.

import numpy as np
import kwker

rows = np.array([(1, 3.5), (2, 1.0), (3, 2.25)], dtype=[("id", "i4"), ("score", "f8")])
kwker.permute_in_place(rows, kwker.argsort(rows["score"]))
print(rows["id"], rows["score"])
[2 3 1] [1.   2.25 3.5 ]