2025, Nov 02 09:01
Топ‑N по рангу в pandas без двойного melt: concat, stack и pivot
Как получить топ‑N по рангу на каждую дату в pandas без двойного melt: компактный прием с concat+stack и pivot, вариант значений через сортировку NumPy.
Преобразование таблицы «оценки по участникам» в представление «топ‑N по рангу на каждую дату» — типичная задача перекройки данных в pandas. Важно удержать логику простой и устойчивой, избегая лишних переходов в «длинный» формат и обратно. Ниже — компактный прием, который решает задачу, а также вариант для случая, когда нужны только сами наибольшие значения.
Постановка задачи
У нас есть ежедневные баллы нескольких участников. Нужно собрать DataFrame: в индексе — даты, в столбцах — места (1, 2, 3), в ячейках — соответствующие значения баллов.
import numpy as np
import pandas as pd
gen = np.random.default_rng(42)
days = pd.date_range('2024-08-01', '2024-08-07')
players = ['Alligator', 'Beryl', 'Chupacabra', 'Dandelion', 'Eggplant', 'Feldspar']
vals_rand = gen.random((len(days), len(players)))
df_scores = pd.DataFrame(vals_rand, index=days, columns=players)
df_scores.index.name = 'DATE'
df_scores.columns.name = 'CONTESTANT'
df_ranks = df_scores.rank(axis=1, method='first', ascending=False)
ranks_top3 = df_ranks.where(df_ranks <= 3)Первый подход (работает, но громоздкий)
Один из способов получить нужную компоновку — расплавить оба фрейма, «пришить» ранги к строкам с баллами, отбросить всё, что не попало в топ‑3, затем развернуть обратно в широкий формат.
long_vals = df_scores.T.melt(value_name='SCORE')
long_ranks = ranks_top3.T.melt(value_name='RANK')
long_vals['RANK'] = long_ranks['RANK']
wide_try = long_vals.dropna().pivot(columns='RANK', index='DATE', values='SCORE')Так мы получаем нужную форму, но двойной melt и ручное выравнивание рангов к баллам легко привести к ошибкам и ухудшают читаемость.
Более чистое преобразование с concat, stack и pivot
Двойного melt можно вовсе избежать: конкатенируйте баллы и ранги бок о бок, один раз выполните stack для выравнивания по участнику, отфильтруйте и затем сделайте pivot.
tidy = (pd.concat({'score': df_scores, 'rank': ranks_top3}, axis=1)
        .stack()
        .dropna(subset=['rank'])
        .droplevel('CONTESTANT'))
final_by_rank = tidy.pivot(columns='rank', values='score')Здесь concat связывает два согласованных DataFrame под ключами score и rank. Один stack поднимает данные уровня участника в индекс, благодаря чему столбцы score и rank выстраиваются по строкам. После фильтрации записей вне топ‑3 и удаления уровня CONTESTANT простой pivot дает широкую таблицу со столбцами‑рангами.
Если нужны только значения топ‑N
Если личности участников и метки рангов не требуются, ранжирование можно пропустить и взять топ N значений на дату напрямую, отсортировав массив NumPy. Этот прием не зависит от уникальности рангов.
arr = df_scores.to_numpy()
arr.sort(axis=1)
K = 3
topn_values = pd.DataFrame(
    arr[:, :-K-1:-1],
    index=df_scores.index,
    columns=pd.Index(range(1, K+1), name='RANK')
)В результате получаем K наибольших баллов за день, упорядоченных от ранга 1 до K, исключительно по величине.
Зачем это нужно
Переформатирование метрик — типичная часть аналитических пайплайнов: построение таблиц лидеров, извлечение лучших результатов, подготовка данных для дашбордов. Связка concat и stack делает преобразование компактным и понятным: измерения выравниваются с полученными метками за один проход. А когда важны только сами значения, небольшой срез NumPy решает задачу без лишних действий и без опоры на семантику рангов.
Практические советы и итог
Отдавайте предпочтение одному, хорошо структурированному преобразованию вместо нескольких melt, если DataFrame уже согласованы. Держите баллы и ранги вместе как можно дольше — так меньше риска рассинхронизации. Если уникальность рангов не гарантирована или не важна, сортировка сырых значений — простой и надежный вариант. И наконец, выбирайте ясные, различимые имена переменных и формируйте массивы сразу в целевой форме — так подготовка данных остаётся очевидной и легче поддерживается.
Статья основана на вопросе на StackOverflow от lubenthrust и ответе от mozway.