2025, Nov 04 21:01
Ранжирование категорий по месяцам в Polars: rank over и pivot
Как в Polars посчитать частоты категорий по месяцам, присвоить ранги с rank over и развернуть результат через pivot. Идиоматичный пример кода и нюансы ничьих.
Когда нужно определить самые частые категории по месяцам в DataFrame Polars и затем разложить их по рангам по месяцам, на бумаге преобразование выглядит простым, но в коде легко разрастается. Задача — посчитать частоты, проставить ранги категориям внутри каждого месяца и развернуть результат так, чтобы каждый столбец представлял месяц, а каждая строка — позицию в ранге.
Пример набора данных и базовый подход
Ниже — компактный датасет с идентификаторами, месяцами и метками категорий. Далее — рабочий, но не самый прямолинейный способ, который получает ранги через накопительную сумму после сортировки.
import polars as pl
records = pl.DataFrame(
{
"rec_id": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17],
"period": [1, 2, 1, 2, 1, 2, 2, 2, 1, 1, 1, 2, 2, 2, 1, 1, 2],
"label": [
"C",
"B",
"A",
"C",
"B",
"A",
"C",
"C",
"A",
"B",
"A",
"A",
"C",
"C",
"A",
"B",
"B",
],
}
)
(
records.group_by(pl.col("period"), pl.col("label"))
.agg(pl.col("rec_id").len().alias("freq"))
.sort(by=["period", "freq"], descending=True)
.with_columns(pl.lit(1).alias("flag"))
.with_columns(pl.col("flag").cum_sum().over(["period"]).alias("rank_idx"))
.pivot(index="rank_idx", values="label", on="period", sort_columns=True)
)
Это решает задачу: сначала мы считаем вхождения, сортируем по частоте внутри каждого месяца, имитируем счётчик по месяцу с помощью накопительной суммы, а затем делаем сводную развёртку. Работает, но не слишком прямо отражает идею «ранга внутри группы».
Что тут на самом деле происходит
Суть задачи — ранжировать количества категорий по месяцам. Хитрость с накопительной суммой даёт индекс, похожий на ранг, но в Polars уже есть встроенная операция ранжирования. Недостающее звено — применить ранг внутри каждого месяца, и именно это делает оконное выражение с over. Есть и тонкость с обработкой равенств: метод "ordinal" разрешает ничьи произвольно. На практике при равных счетчиках в столбце конкретного месяца можно увидеть, например, порядок CBA вместо CAB.
Более простой вариант с rank и over
Ниже — более лаконичный и идиоматичный конвейер, который считает, ранжирует по месяцам и разворачивает результат в одной цепочке.
(
records.group_by("period", "label")
.len()
.with_columns(
pl.col("len").rank("ordinal", descending=True).over("period").alias("rank_idx")
)
.sort("rank_idx")
.pivot("period", index="rank_idx", values="label", sort_columns=True)
)
Практическая деталь: результирующий rank_idx имеет тип u32, а не i64; если для последующей логики важен тип целого, можно выполнить приведение в конце.
Почему это важно
Использование rank с оконным выражением по месяцу убирает лишнюю сложность и прямо выражает намерение. Такой конвейер легче читать, объяснять и сопровождать, особенно когда шаг ранжирования — ключевой в преобразовании. К тому же поведение при равенствах становится явным благодаря выбранному методу ранжирования.
Выводы
Для ранжирования внутри групп в Polars предпочтительнее rank с over, а не построение рангов косвенно через накопительные суммы. Посчитайте по (месяц, категория), ранжируйте счётчики по убыванию внутри месяца, отсортируйте по этому рангу и сделайте сводную развёртку, чтобы выровнять категории по рангам между месяцами. Помните, что ordinal-ранжирование разбирается с ничьими произвольно, и при необходимости приводите тип ранга к нужному в итоговой схеме.
Статья основана на вопросе на StackOverflow от Simon и ответе от BallpointBen.