Сортировка, топы и подсчёты: 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 принимает имя столбца и возвращает строки таблицы, упорядоченные по нему. По умолчанию — по возрастанию, а в отчётах почти всегда нужно наоборот, поэтому привычная запись выглядит так:
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
Вот грабли, о которые ударяется каждый второй. После сортировки индекс едет вместе со строками — и обращение по метке начинает возвращать не то, что вы ожидали:
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())
1. Как отсортировать DataFrame по столбцу revenue от большего к меньшему?
2. Что возвращает orders["city"].value_counts()?
3. Как получить 5 самых дорогих заказов, не сортируя всю таблицу?
4. Что изменит параметр normalize=True в value_counts?
5. Как rank(method="min") расставит два одинаковых максимальных значения?
Соберите DataFrame из выгрузки пяти заказов (данные уже в заготовке), добавьте столбец revenue = price * qty и выведите: 1) топ-2 заказа по выручке со столбцами order_id, product, revenue через nlargest; 2) частоты городов через value_counts.
Как отсортировать 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-канал или свой блог — так о проекте узнают новые читатели.
Что читать дальше
Pandas · Урок 5
Группировка groupby в Pandas: агрегация как в Excel, только мощнее
groupby, agg и pivot_table — считаем выручку по городам и товарам, строим сводные таблицы и не теряем строки с NaN.
Pandas · Урок 3
Выборка данных в Pandas: loc, iloc и фильтрация строк по условию
loc и iloc, булева маска, isin и between — достаём из таблицы заказов именно те строки и столбцы, которые нужны, и не попадаемся на SettingWithCopyWarning.
Pandas · Урок 7
Объединение таблиц: merge, concat и join в Pandas
merge с параметром how как SQL JOIN, concat для склейки выгрузок и join по индексу — соединяем товары, заказы и клиентов в одну таблицу и не теряем строки по дороге.
Похожие уроки по темам
Подобраны автоматически по пересечению тем и ключевых слов.
Matplotlib / Seaborn · Урок 7
Seaborn для начинающих: красивые графики одной строкой
Seaborn рисует статистические графики из DataFrame одной строкой: темы set_theme, countplot, barplot с доверительными интервалами и разбивка hue — прямо в браузере.
seaborn для начинающихpandas dataframe график
openpyxl · Урок 11
data_only=True: читаем вычисленные значения из Excel
data_only=True открывает книгу в режиме значений: формулы заменяются тем, что Excel посчитал при последнем сохранении, — и если файла Excel не касался, вместо числа будет None.
openpyxl формулы значенияпрочитать excel python значения
Pandas · Урок 8
Новые столбцы в Pandas: apply, map и векторные операции над колонками
Добавляем в DataFrame выручку, скидки и категории: векторные операции, np.where, map и apply — и разбираемся, почему apply по строкам почти всегда лишний.
pandas новый столбецкак добавить новый столбец в dataframe