Однажды мне понадобилось вычислить ранги элементов массива, или другими словами, на какой позиции окажется каждый элемент после сортировки.
Беглый поиск по интернету дал неожиданно короткий ответ: достаточно применить двойной argsort —argsort(argsort(x)) из библиотеки NumPy. Фокус работает, но почти нигде не объясняется почему именно. Обращаться с таким вопросом к ChatGPT тогда казалось рискованным — в интернете как раз ходили мемы о его ошибках в арифметике. Отсутствие строгого обоснования не давало мне покоя, и в итоге я решил дойти до доказательства самостоятельно.
Ниже я поделюсь этим доказательством. Оно не претендует на уникальность — возможно, существуют более короткие или более элегантные способы прийти к тому же ответу. Тем не менее, оно позволит вам понять, почему двойной argsort действительно возвращает ранги элементов, и избавит вас от необходимости тратить время на самостоятельное разбирательство.
На самом деле в Python в библиотеке scipy уже есть функция rankdata, которая реализует то, что нам нужно. Единственное отличие — rankdata возвращает ранги начиная с 1, тогда как argsort использует нулевую индексацию. Чуть ниже мы сравним их результаты, а пока формализуем нашу задачу.
Основные определения
Пусть задан массив:
- его отсортированная версия
- результат работы
argsort(a)
- результат работы
argsort(argsort(a))
- ранги массива
Отдельно отмечу, что если для
, то
также показывает, сколько элементов массива меньше данного, ведь для
верно следующее
. Если же в массиве присутствуют одинаковые элементы, то встает вопрос, какой ранг им присвоить. Двойной аргсорт будет выдавать ранг по исходной позиции элемента: чем меньше индекс в исходном массиве, тем меньше итоговый ранг. Поэтому значение ранга у него уникально для каждого элемента, но оно уже не равно количеству элементов, меньших данного. В
rankdata из scipy можно настроить этот момент под конкретную задачу через переменную method.
Для наглядности рассмотрим пример
Чтобы получить от rankdata такой же ответ как от argsort мы укажем в качестве method='ordinal' а также вычтем единицу, чтобы его ответы лежали в том же множестве.
import numpy as np from scipy.stats import rankdata a = np.array([13, 0, 47, 52, 27]) print("a:", a) s = np.sort(a) print("s = sort(a):", s) m = np.argsort(a) print("m = argsort(a):", m) p = np.argsort(m) print("p = argsort(argsort(a)):", p) r = rankdata(a, method="ordinal") - 1 print("r = rank(a):", r)
a: [13 0 47 52 27] s = sort(a): [0 13 27 47 52] m = argsort(a): [1 0 4 2 3] p = argsort(argsort(a)): [1 0 3 4 2] r = rank(a): [1 0 3 4 2]
Из определения argsort следует, что , т.е.
- это индекс элемента массива a, который после сортировки будет на позиции i в отсортированном массиве s.
Мы же хотим найти ранг , который показывает позицию (индекс), которую элемент
занимает после сортировки. В терминах введенных выше обозначений
.
Утверждается, что , докажем это.
Доказательство
Из определения argsort следует, что массив m является перестановкой индексов .
То есть:
Рассмотрим массив:
По определению argsort индексы p упорядочивают элементы массива m по не убыванию:
Но из условия (1) мы знаем, что равенство невозможно, и итоговая последовательность будет представлять из себя упорядоченные индексы от 1 до , т.е.
Следовательно, получаем ключевое равенство:
Теперь воспользуемся определением сортировки массива :
Подставим сюда
А че, так можно было?
p также является перестановкой чисел , следовательно,
подходит на роль индекса и мы можем им заменить i в нашем выражении выше
Но, используя полученное ранее равенство (2), левая часть предыдущего уравнения упрощается, и мы получаем:
Это означает, что индекс показывает позицию элемента
в отсортированном массиве s.
По определению это и есть ранг элемента.
Таким образом,
То есть двойной argsort действительно возвращает ранги элементов массива.
