Перейти к содержанию

172 уроков, 13 библиотек и челлендж «Что выведет код?» — бесплатно, код прямо в браузере

Начать обучение
Урок 6 из 10 Средний 35 мин 120 XP

Сортировка, топы и подсчёты: sort_values, value_counts, rank

sort_values по нескольким столбцам, nlargest вместо сортировки всей таблицы, частоты value_counts и рейтинги rank — собираем отчёты «топ-10 товаров» и «доли городов».

Редакция Питоники

Директор пишет вам в 17:58: «Нужны к утру топ продаж по выручке, три лучших товара в каждой категории и сколько процентов выручки даёт каждый город». В Excel это вечер фильтров, копирований и одной сломанной сводной. В pandas это четыре строки кода — и все они сегодня встретятся вам в этом уроке.

Мы уже умеем доставать нужные строки через loc, iloc и фильтрацию и сворачивать данные через groupby. Остался последний кирпич быстрых отчётов — упорядочить и посчитать: сортировка sort_values, топ-N через nlargest, частоты value_counts и рейтинги rank. Работаем по-прежнему на данных нашего интернет-магазина: восемь заказов, для которых уже посчитана выручка revenue как price * qty.

sort_values: сортировка DataFrame по столбцу

Главный вопрос любого отчёта — «кто наверху». Метод sort_values принимает имя столбца и возвращает строки таблицы, упорядоченные по нему. По умолчанию — по возрастанию, а в отчётах почти всегда нужно наоборот, поэтому привычная запись выглядит так:

Топ-3 заказа по выручке
import pandas as pd

orders = pd.DataFrame({
    "order_id": [1001, 1002, 1003, 1004, 1005, 1006, 1007, 1008],
    "city": ["Москва", "Казань", "Сочи", "Москва", "Казань", "Сочи", "Москва", "Казань"],
    "product": ["Ноутбук", "Смартфон", "Наушники", "Планшет", "Смартфон", "Ноутбук", "Наушники", "Планшет"],
    "price": [54990, 24990, 8990, 31990, 8990, 57990, 9490, 29990],
    "qty": [1, 2, 3, 1, 3, 1, 2, 2],
})
orders["revenue"] = orders["price"] * orders["qty"]

top3 = orders.sort_values("revenue", ascending=False).head(3)
print(top3)
Вывод
   order_id    city  product  price  qty  revenue
7      1008  Казань  Планшет  29990    2    59980
5      1006    Сочи  Ноутбук  57990    1    57990
0      1001  Москва  Ноутбук  54990    1    54990

Параметр ascending=False переворачивает порядок — без него вы получили бы три худших заказа, что тоже иногда нужно, но реже. Обратите внимание на левый столбец: pandas переставил строки вместе с их индексами — 7, 5, 0. Вывод сортировки — это новая таблица-копия; исходный orders остаётся в исходном порядке, и это правильно: сортировка — взгляд на данные, а не их поломка. Если результат нужен под именем df, просто переприсвойте: df = df.sort_values(...).

А где окажутся пропуски? Pandas по умолчанию относит NaN в конец независимо от направления сортировки — удобно для просмотра, но если вам нужны пропуски сверху, задайте na_position="first". Мы в этом уроке считаем, что данные уже почищены; как чистить — тема урока про пропуски и дубликаты.

Сортировка по нескольким столбцам

Реальные отчёты почти никогда не сортируются по одному столбцу. «Покажи заказы по городам, а внутри каждого города — от самых прибыльных» — вот классическая задача для sort_values со списком. Правило простое: столбцы передаются списком, и точно так же списком передаются направления сортировки — по одному значению True/False на каждый столбец:

Город по алфавиту, выручка внутри города по убыванию
import pandas as pd

orders = pd.DataFrame({
    "order_id": [1001, 1002, 1003, 1004, 1005, 1006, 1007, 1008],
    "city": ["Москва", "Казань", "Сочи", "Москва", "Казань", "Сочи", "Москва", "Казань"],
    "product": ["Ноутбук", "Смартфон", "Наушники", "Планшет", "Смартфон", "Ноутбук", "Наушники", "Планшет"],
    "price": [54990, 24990, 8990, 31990, 8990, 57990, 9490, 29990],
    "qty": [1, 2, 3, 1, 3, 1, 2, 2],
})
orders["revenue"] = orders["price"] * orders["qty"]

report = orders.sort_values(
    ["city", "revenue"], ascending=[True, False]
)[["city", "product", "revenue"]]
print(report.head(6))
Вывод
     city   product  revenue
7  Казань   Планшет    59980
1  Казань  Смартфон    49980
4  Казань  Смартфон    26970
0  Москва   Ноутбук    54990
3  Москва   Планшет    31990
6  Москва  Наушники    18980

Первый ключ — city по возрастанию (по алфавиту: Казань, Москва, Сочи), второй — revenue по убыванию внутри каждого города. Длина списков должна совпадать: два столбца — два направления, иначе pandas ответит ошибкой длины. Такой шаблон «группа по возрастанию, метрика внутри группы по убыванию» вы будете использовать чаще любого другого — запомните именно его.

Сортировка ломает индекс: осторожно с loc

Вот грабли, о которые ударяется каждый второй. После сортировки индекс едет вместе со строками — и обращение по метке начинает возвращать не то, что вы ожидали:

loc ищет по метке, iloc — по позиции
import pandas as pd

orders = pd.DataFrame({
    "order_id": [1001, 1002, 1003, 1004, 1005, 1006, 1007, 1008],
    "city": ["Москва", "Казань", "Сочи", "Москва", "Казань", "Сочи", "Москва", "Казань"],
    "product": ["Ноутбук", "Смартфон", "Наушники", "Планшет", "Смартфон", "Ноутбук", "Наушники", "Планшет"],
    "price": [54990, 24990, 8990, 31990, 8990, 57990, 9490, 29990],
    "qty": [1, 2, 3, 1, 3, 1, 2, 2],
})
orders["revenue"] = orders["price"] * orders["qty"]

top = orders.sort_values("revenue", ascending=False)
print(top.index.tolist())     # старые номера в новом порядке
print(top.loc[0, "product"])  # метка 0 - это исходная первая строка
print(top.iloc[0, 2])         # позиция 0 - реальный лидер таблицы
Вывод
[7, 5, 0, 1, 3, 2, 4, 6]
Ноутбук
Планшет

Как сбросить индекс после сортировки в pandas?

Строка с меткой 0 действительно существует в отсортированной таблице — просто стоит она не первой, а третьей. Поэтому top.loc[0] вернул Ноутбук за 54 990, хотя наверху таблицы лежит Планшет за 59 980. Разницу между loc и iloc мы подробно разбирали в уроке про выборку строк, а здесь практический вывод: после сортировки, из которой вы хотите брать строки «по порядку», сбросьте индекс. Параметр drop=True говорит не сохранять старые номера отдельным столбцом:

Сброс индекса после сортировки
top_clean = orders.sort_values("revenue", ascending=False).reset_index(drop=True)
print(top_clean.head(3))
Вывод
   order_id    city  product  price  qty  revenue
0      1008  Казань  Планшет  29990    2    59980
1      1006    Сочи  Ноутбук  57990    1    57990
2      1001  Москва  Ноутбук  54990    1    54990

Топ-N без сортировки: nlargest и nsmallest

Когда из миллиона строк нужны только десять лучших, сортировать всё — расточительно. Метод nlargest берёт прямо отбором N наибольших значений столбца, а nsmallest — N наименьших. Результат тот же, что у sort_values(...).head(N), только быстрее и короче:

Лучшее и худшее без сортировки всей таблицы
import pandas as pd

orders = pd.DataFrame({
    "order_id": [1001, 1002, 1003, 1004, 1005, 1006, 1007, 1008],
    "city": ["Москва", "Казань", "Сочи", "Москва", "Казань", "Сочи", "Москва", "Казань"],
    "product": ["Ноутбук", "Смартфон", "Наушники", "Планшет", "Смартфон", "Ноутбук", "Наушники", "Планшет"],
    "price": [54990, 24990, 8990, 31990, 8990, 57990, 9490, 29990],
    "qty": [1, 2, 3, 1, 3, 1, 2, 2],
})
orders["revenue"] = orders["price"] * orders["qty"]

print(orders.nlargest(3, "revenue")[["product", "revenue"]])
print(orders.nsmallest(2, "price")[["product", "price"]])
Вывод
   product  revenue
7  Планшет    59980
5  Ноутбук    57990
0  Ноутбук    54990
    product  price
2  Наушники   8990
4  Смартфон   8990

Дешёвые Наушники и Смартфон стоят одинаково — 8 990 рублей, — и pandas по умолчанию оставил ту, что встретилась в данных первой. Если же в отчёте нужны все кандидаты с одинаковым значением на границе топа, добавьте параметр keep="all": orders.nlargest(3, "revenue", keep="all") вернёт больше трёх строк, лишь бы никто из лидеров не потерялся.

value_counts: как часто встречаются значения

Вторая половина отчётов — не «что дороже», а «чего сколько». Сколько заказов из какого города, какой товар покупают чаще. Для одного столбца это метод value_counts: он возвращает Series, где индекс — уникальные значения, а данные — их количество, сразу отсортированный по убыванию:

Частоты городов
import pandas as pd

orders = pd.DataFrame({
    "order_id": [1001, 1002, 1003, 1004, 1005, 1006, 1007, 1008],
    "city": ["Москва", "Казань", "Сочи", "Москва", "Казань", "Сочи", "Москва", "Казань"],
    "product": ["Ноутбук", "Смартфон", "Наушники", "Планшет", "Смартфон", "Ноутбук", "Наушники", "Планшет"],
    "price": [54990, 24990, 8990, 31990, 8990, 57990, 9490, 29990],
    "qty": [1, 2, 3, 1, 3, 1, 2, 2],
})

print(orders["city"].value_counts())
Вывод
city
Москва    3
Казань    3
Сочи      2
Name: count, dtype: int64

Москва и Казань делят первое место по три заказа, Сочи отстаёт на один. Заметьте: value_counts работает у Series, то есть у одного столбца, — вызвать его у всей таблицы «про запас» не получится, нужно выбрать столбец явно. Результат уже отсортирован, поэтому «самый частый» — это всегда первое значение, и orders["city"].value_counts().head(1) — готовый ответ на вопрос «откуда наши заказы».

Доли и проценты: normalize=True

Абсолютные числа хорошо смотрятся в таблице, но для презентации нужны доли. Параметр normalize=True делит каждое значение count на общую сумму — и вы получаете частоты от 0 до 1:

Доли городов
print(orders["city"].value_counts(normalize=True).round(2))
Вывод
city
Москва    0.38
Казань    0.38
Сочи      0.25
Name: proportion, dtype: float64

Доли надо округлять — иначе получите 0.37500000000000006 и похожие честные, но нечитаемые хвосты плавающей точки. Для процентов умножьте на сто: value_counts(normalize=True).mul(100).round(1). Это по-прежнему доля попадания в город случайного заказа; если нужны не штуки, а рубли, value_counts не подойдёт — берите groupby, как в прошлом уроке.

Подсчёт сразу по двум столбцам

У value_counts есть параметр subset — список столбцов, пары которых нужно посчитать. Результат выходит с двухуровневым индексом: сначала город, внутри — товар:

Пары «город - товар»
print(orders.value_counts(subset=["city", "product"]))
Вывод
city    product 
Казань  Смартфон    2
        Планшет     1
Москва  Наушники    1
        Ноутбук     1
        Планшет     1
Сочи    Наушники    1
        Ноутбук     1
Name: count, dtype: int64

Читается это так: в Казани дважды покупали Смартфон, остальные комбинации встретились по одному разу. Тот же результат даёт groupby по двум столбцам с size(); value_counts удобнее, когда итог сразу нужен отсортированным.

Пропуски в подсчёте: до и после
import pandas as pd
import numpy as np

cities = pd.Series(["Москва", "Казань", np.nan, "Сочи", "Казань", np.nan])
print(cities.value_counts())             # NaN исчезли молча
print(cities.value_counts(dropna=False)) # теперь NaN видно
Вывод
Казань    2
Москва    1
Сочи      1
Name: count, dtype: int64
Казань    2
NaN       2
Москва    1
Сочи      1
Name: count, dtype: int64

А в первой версии видно четыре значения — и кажется, что данные полны. Два пропуска из шести — это треть выгрузки, которую вы бы не заметили. Отсюда правило: любой подсчёт частот на грязных данных делайте с dropna=False, а чистить пропуски мы учились в уроке про NaN и дубликаты.

sort_index: когда итог нужен в исходном порядке

value_counts сортирует по количеству, но иногда нужен алфавитный или «справочный» порядок — например, чтобы таблица городов совпадала с порядком в другом отчёте. Тогда сортируют не значения, а индекс:

Города по алфавиту
counts = orders["city"].value_counts()
print(counts.sort_index())
Вывод
city
Казань    3
Москва    3
Сочи      2
Name: count, dtype: int64

sort_index принимает те же параметры, что и sort_values, включая ascending=False для обратного порядка. Пара «value_counts + sort_index» — стандартная заготовка для бар-чартов, где категории принято показывать по порядку, а не по величине.

rank: рейтинг строк вместо сортировки

Сортировка меняет порядок строк, а rank оставляет таблицу на месте и дописывает к ней столбец с номерами мест. Это удобно, когда в рабочей таблице нужен постоянный рейтинг — например, место каждого заказа по выручке:

Столбец с местами
import pandas as pd

orders = pd.DataFrame({
    "order_id": [1001, 1002, 1003, 1004, 1005, 1006, 1007, 1008],
    "city": ["Москва", "Казань", "Сочи", "Москва", "Казань", "Сочи", "Москва", "Казань"],
    "product": ["Ноутбук", "Смартфон", "Наушники", "Планшет", "Смартфон", "Ноутбук", "Наушники", "Планшет"],
    "price": [54990, 24990, 8990, 31990, 8990, 57990, 9490, 29990],
    "qty": [1, 2, 3, 1, 3, 1, 2, 2],
})
orders["revenue"] = orders["price"] * orders["qty"]

orders["rev_rank"] = orders["revenue"].rank(ascending=False, method="min").astype(int)
print(orders.sort_values("rev_rank")[["product", "revenue", "rev_rank"]].head(5))

sales = pd.Series([26970, 26970, 18980], index=["Наушники", "Смартфон", "Чехол"])
print(sales.rank(ascending=False, method="average"))
print(sales.rank(ascending=False, method="min"))
Вывод
    product  revenue  rev_rank
7   Планшет    59980         1
5   Ноутбук    57990         2
0   Ноутбук    54990         3
1  Смартфон    49980         4
3   Планшет    31990         5
Наушники    1.5
Смартфон    1.5
Чехол       3.0
dtype: float64
Наушники    1.0
Смартфон    1.0
Чехол       3.0
dtype: float64

ascending=False даёт «место 1 у самого большого» — как в спорте. А вот параметры method определяют судьбу одинаковых значений, и их стоит знать по именам:

  • method="average" (по умолчанию) — делят место пополам: два лидера получают по 1.5;
  • method="min" — обоим достаётся лучшее общее место, следующее значение получает 3, как на соревнованиях;
  • method="dense" — то же, но следующее значение получает 3 без разрыва номеров;
  • method="first" — никто не делит места: порядок как в данных.

Ещё одна опция — pct=True: вместо номеров мест rank вернёт долю от общего числа строк, от 0 до 1. Так отвечают на вопрос «к этому заказу относится топ-10% выручки»: orders["revenue"].rank(pct=True, ascending=False) <= 0.1 — готовая булева маска для фильтрации из урока про loc и iloc.

Мини-кейс: лучший товар в каждой категории

Соберём приём, ради которого обычно и учат сортировку. Задача: для каждого товара показать его лучший заказ по выручке. В SQL это решается через оконные функции, а в pandas — связкой сортировки и drop_duplicates: сначала упорядочиваем по убыванию выручки, потом оставляем первую строку для каждой группы:

Топ-заказ каждого товара
import pandas as pd

orders = pd.DataFrame({
    "order_id": [1001, 1002, 1003, 1004, 1005, 1006, 1007, 1008],
    "city": ["Москва", "Казань", "Сочи", "Москва", "Казань", "Сочи", "Москва", "Казань"],
    "product": ["Ноутбук", "Смартфон", "Наушники", "Планшет", "Смартфон", "Ноутбук", "Наушники", "Планшет"],
    "price": [54990, 24990, 8990, 31990, 8990, 57990, 9490, 29990],
    "qty": [1, 2, 3, 1, 3, 1, 2, 2],
})
orders["revenue"] = orders["price"] * orders["qty"]

best = orders.sort_values("revenue", ascending=False).drop_duplicates("product")
print(best[["product", "revenue"]])
Вывод
    product  revenue
7   Планшет    59980
5   Ноутбук    57990
1  Смартфон    49980
2  Наушники    26970

drop_duplicates("product") по умолчанию хранит первую встреченную строку каждой группы — а после сортировки первой стоит самая выгодная. Приём «sort_values + drop_duplicates» заменяет половину оконных функций и одинаково работает для «лучший заказ клиента», «последняя цена товара» (тогда сортируйте по дате) и «самый свежий статус заявки». На собеседованиях аналитиков это спрашивают постоянно.

Единственная оговорка: если у двух строк группы выручка совпадает в точности, побеждает та, что раньше встретилась в данных, — поведение keep="first". Когда правило «первый в таблице» не годится, добавьте третий уровень сортировки (например, по order_id) и сделайте выбор детерминированным: отчёт, который даёт разные ответы на одни данные, хуже любого бага.

Что дальше: объединение таблиц

Мы отсортировали, посчитали и отрейтинговали — но всё ещё работали с одной таблицей. Реальный магазин хранит заказы, справочник товаров и клиентов в разных файлах, и выручку из прошлого примера пришлось бы считать руками, потому что цены лежат в другом месте. В следующем уроке разберём merge, concat и join — как SQL JOIN, только парой строк Python: склеим товары, заказы и клиентов в одну таблицу и заодно узнаем, почему merge без параметра how тихо удаляет строки. А пока — тест и практика: соберите утренний отчёт для директора сами. Пришли из поиска за одним рецептом сортировки? Полный маршрут — самоучитель Pandas с нуля: десять уроков от первой Series до сквозного проекта, сортировка здесь шестая ступень.

Что выведет код?

Сначала предскажи ответ в голове — это главный навык программиста.

import pandas as pd

s = pd.Series(["a", "b", "a", "c", "b", "a"])
print(s.value_counts())
import pandas as pd

s = pd.Series([10, 20, 20, 30])
print(s.rank(ascending=False, method="min").tolist())
Проверь себя
0 / 5

1. Как отсортировать DataFrame по столбцу revenue от большего к меньшему?

2. Что возвращает orders["city"].value_counts()?

3. Как получить 5 самых дорогих заказов, не сортируя всю таблицу?

4. Что изменит параметр normalize=True в value_counts?

5. Как rank(method="min") расставит два одинаковых максимальных значения?

Карточки терминов
Запомнено: 0 / 6
Практика

Соберите DataFrame из выгрузки пяти заказов (данные уже в заготовке), добавьте столбец revenue = price * qty и выведите: 1) топ-2 заказа по выручке со столбцами order_id, product, revenue через nlargest; 2) частоты городов через value_counts.

practice.py
Вопросы и ответы по уроку

Как отсортировать DataFrame в Pandas по столбцу?

Методом `df.sort_values("имя_столбца")`. По умолчанию сортировка по возрастанию; для убывания добавьте ascending=False. По нескольким столбцам передайте списки: `df.sort_values(["city", "price"], ascending=[True, False])` — город по алфавиту, цена внутри по убыванию.

Чем nlargest отличается от sort_values().head()?

Результат одинаковый, но `nlargest(5, "price")` отбирает пять наибольших значений напрямую, без упорядочивания всей таблицы — на больших данных это заметно быстрее. Кроме того, у nlargest есть параметр `keep="all"`, который сохраняет строки с одинаковыми значениями на границе топа.

Что делает normalize=True в value_counts?

Заменяет абсолютные количества долями: каждое значение делится на общую сумму, поэтому доли в сумме дают 1. Для процентов умножьте результат на сто. Важно: normalize считается от значений, оставшихся после отбрасывания NaN, поэтому на грязных данных передавайте ещё и dropna=False.

Почему value_counts не показывает NaN?

По умолчанию value_counts пропускает пропущенные значения (dropna=True), и таблица частот выглядит полной, даже если треть данных пуста. Передайте value_counts(dropna=False), и NaN появится в результате отдельной строкой — так вы сразу увидите масштаб проблемы.

Понравился урок? Сошлитесь на него

«Индекс pandas — это метки, а не позиции.»

Скопируйте готовую ссылку в формате HTML, Markdown или чистый адрес и вставьте в статью на Habr, VC, Telegram-канал или свой блог — так о проекте узнают новые читатели.

TelegramVK

Похожие уроки по темам

Подобраны автоматически по пересечению тем и ключевых слов.