Урок курса

Сортировка массивов: sort и argsort

NumPy и pandas: практический тренажёр

В прошлом уроке мы разбирали, как объединять массивы через concatenate и stack, и постоянно проверяли результат через .shape — это был главный способ убедиться, что склейка прошла корректно. Сейчас переходим к сортировке, и параметр axis здесь работает по той же логике: он указывает, вдоль какой оси идёт операция.

np.sort: получение отсортированной копии по оси

Функция np.sort возвращает новый массив с теми же элементами, расположенными в порядке возрастания. Исходный массив при этом не трогается — это важно запомнить сразу, потому что в numpy существует и метод .sort(), который сортирует массив «на месте». Здесь речь только о функции-копии.

Для одномерного массива всё просто:

import numpy as np

arr = np.array([30, 10, 40, 20])
sorted_arr = np.sort(arr)
print(sorted_arr)  # [10 20 30 40]
print(arr)         # [30 10 40 20] — не изменился

Shape результата совпадает с shape входа: был (4,), стал (4,).

С двумерным массивом поведение определяется параметром axis.

M = np.array([[3, 1, 4],
              [1, 5, 2]])

axis=0 — каждый столбец сортируется независимо, элементы упорядочиваются сверху вниз:

print(np.sort(M, axis=0))
# [[1 1 2]
#  [3 5 4]]

Столбец 0: [3, 1][1, 3]. Столбец 1: [1, 5][1, 5]. Столбец 2: [4, 2][2, 4]. Строки при этом как единицы не сохраняются — элементы из одной строки могут «разойтись» по разным позициям.

axis=1 — каждая строка сортируется независимо, элементы упорядочиваются слева направо:

print(np.sort(M, axis=1))
# [[1 3 4]
#  [1 2 5]]

Строка 0: [3, 1, 4][1, 3, 4]. Строка 1: [1, 5, 2][1, 2, 5].

В обоих случаях shape результата остаётся (2, 3) — таким же, как у исходного M. np.sort никогда не меняет форму массива при использовании числового axis.

Практическое правило выбора оси: определите, какие последовательности должны быть отсортированы. Если нужно упорядочить элементы внутри каждой строки — используйте axis=1. Если нужно упорядочить элементы внутри каждого столбца — используйте axis=0. Нужная ось зависит от структуры ваших данных и задачи.

np.argsort: индексы порядка элементов

np.sort отвечает на вопрос «каковы значения в отсортированном порядке?». np.argsort отвечает на другой вопрос: «в каком порядке нужно взять элементы исходного массива, чтобы они оказались отсортированы?». Результат — не значения, а целочисленные индексы.

Для одномерного массива:

import numpy as np

arr = np.array([30, 10, 40, 20])
print(np.argsort(arr))  # [1 3 0 2]

Читается так: на первом месте стоит элемент с индексом 1 (это 10), на втором — с индексом 3 (это 20), на третьем — с индексом 0 (это 30), на четвёртом — с индексом 2 (это 40). Если применить этот результат обратно к arr, получим отсортированный массив:

idx = np.argsort(arr)
print(arr[idx])  # [10 20 30 40]

То есть np.argsort — это не альтернативный способ получить те же [10, 20, 30, 40], а способ узнать откуда они пришли.

Для двумерного массива поведение управляется параметром axis, как и у np.sort.

M = np.array([[3, 1, 4],
              [1, 5, 2]])

print(np.argsort(M, axis=1))
# [[1 0 2]
#  [0 2 1]]

При axis=1 каждая строка обрабатывается независимо. В строке 0 значения [3, 1, 4]: наименьшее — на позиции 1 (это 1), затем позиция 0 (это 3), затем позиция 2 (это 4). В строке 1 значения [1, 5, 2]: порядок — 0, 2, 1. Каждый индекс — это позиция внутри своей строки.

При axis=0 картина другая: каждый столбец сортируется независимо, а индексы показывают номер строки внутри своего столбца.

print(np.argsort(M, axis=0))
# [[1 0 1]
#  [0 1 0]]

Столбец 0: значения [3, 1] — наименьшее в строке 1, большее в строке 0. Столбец 1: [1, 5] — наименьшее в строке 0. Столбец 2: [4, 2] — наименьшее в строке 1.

Главное отличие от np.sort: результат np.argsort сам по себе нельзя интерпретировать как данные — это карта перестановки, инструмент для последующей индексации.

Shape результатов sort и argsort: анализ по осям

Одно свойство объединяет np.sort и np.argsort: обе функции сохраняют форму входного массива. Какую бы ось вы ни указали, shape результата будет совпадать с shape входа.

Проверим это на конкретных числах.

import numpy as np

# Одномерный массив shape (4,)
arr = np.array([30, 10, 40, 20])

print(np.sort(arr).shape)     # (4,)
print(np.argsort(arr).shape)  # (4,)

# Двумерный массив shape (2, 3)
M = np.array([[3, 1, 4],
              [1, 5, 2]])

print(np.sort(M, axis=0).shape)     # (2, 3)
print(np.sort(M, axis=1).shape)     # (2, 3)
print(np.argsort(M, axis=0).shape)  # (2, 3)
print(np.argsort(M, axis=1).shape)  # (2, 3)

Во всех случаях результат — тот же (4,) или (2, 3). Смена axis меняет смысл операции (что с чем сравнивается), но не форму выхода.

Почему это важно? Потому что после сортировки или получения индексов вы можете сразу применять результат к другим операциям, не пересчитывая shape вручную. Если вошёл массив (2, 3), вышел массив (2, 3) — это гарантия, а не случайность.

Отдельно стоит отметить: это справедливо только при указании числового axis0, 1 и так далее. Есть режим axis=None, который сначала разворачивает массив в одномерный и сортирует его целиком, — но в этом уроке он не используется, и его поведение принципиально другое.

Практический способ проверки: после любого вызова np.sort или np.argsort сразу смотрите на .shape результата. Если входной массив имел shape (r, c), выходной должен тоже показать (r, c). Расхождение означает, что где-то затесался axis=None или лишний вызов flatten.

Перестановка связанного массива через argsort и fancy indexing

Теперь, когда понятно, что возвращает np.argsort, можно использовать этот результат как инструмент. Типичная задача: есть два массива одинаковой длины — один с числовыми значениями, другой со связанными метками. Нужно отсортировать их согласованно, чтобы порядок меток соответствовал порядку значений.

Пример: массив scores содержит результаты пяти участников, массив names — их имена. Хочется расставить имена по возрастанию результата.

import numpy as np

scores = np.array([72, 45, 91, 60, 83])
names  = np.array(['Алиса', 'Борис', 'Вера', 'Глеб', 'Дина'])

idx = np.argsort(scores)
print(idx)           # [1 3 0 4 2]

print(scores[idx])   # [45 60 72 83 91]
print(names[idx])    # ['Борис' 'Глеб' 'Алиса' 'Дина' 'Вера']

Что происходит: np.argsort(scores) вычисляет, в каком порядке нужно взять элементы scores, чтобы они шли по возрастанию. Этот порядок записывается в idx. Затем тот же idx применяется к names через fancy indexing — и массив имён переставляется точно в том же порядке.

Оба массива остаются неизменными — мы получаем две новые перестановки. Никаких циклов, никакого ручного отслеживания позиций.

Важные условия, при которых это работает:

  • оба массива одномерные;
  • оба имеют одинаковую длину;
  • idx получен из np.argsort одного из них.

Если эти условия выполнены, arr[idx] и labels[idx] дадут результаты с тем же shape (n,), что и исходные массивы, и их элементы будут попарно соответствовать друг другу.

Эта техника часто встречается, когда нужно ранжировать объекты по одному признаку, сохраняя при этом их идентификаторы или другие атрибуты. Позже, в pandas, аналогичную задачу решает sort_values для DataFrame — но там это происходит под капотом, а здесь вы видите механику явно.

Попробуйте решить

Массив arr = np.array([50, 20, 40, 10, 30]). Что вернёт np.argsort(arr)?

Продолжить с проверкой и прогрессом

Откройте интерактивный раннер с заданиями урока.

Перейти к интерактивному уроку