0Pricing
Pandas & NumPy Academy · Урок

Ранги и процентили внутри групп

Вычисляйте ранги внутри групп с помощью groupby().rank() и создавайте интервалы процентилей с помощью pd.qcut для относительного сравнения.

«Ранги и процентили внутри групп» — бесплатный урок Pandas & NumPy Academy на CoddyKit. Это урок 4 из 4. Ты можешь прочитать весь урок бесплатно ниже — а потом практиковать его прямо в браузере с встроенным редактором кода и ИИ-репетитором 24/7. Это часть пути обучения Pandas & NumPy Academy, и твой прогресс синхронизируется между веб-версией и приложением CoddyKit. Курс Pandas & NumPy Academy содержит 4 уроков всего.

Зачем вычислять ранги внутри групп?

Исходные значения часто менее информативны, чем относительные ранги. Торговый представитель с ежемесячной выручкой 50 000 долларов является одним из лучших, если средняя выручка составляет 30 000 долларов, но слабым сотрудником, если средняя выручка равна 80 000 долларов. Вычисляя ранги внутри групп (например, ранг в каждом регионе или каждом квартале), Вы получаете нормализованное сравнение, учитывающее разные базовые уровни в группах. Pandas упрощает вычисление рангов внутри групп с помощью groupby().rank().

import pandas as pd
import numpy as np

np.random.seed(42)
df = pd.DataFrame({
    'rep': ['Alice', 'Bob', 'Carol', 'Dave', 'Eve',
            'Frank', 'Grace', 'Hank', 'Iris', 'Jake'],
    'region': ['East']*5 + ['West']*5,
    'sales': np.random.randint(30000, 100000, 10)
})
print(df.sort_values('region'))

Series.rank() — базовое ранжирование

Series.rank() присваивает ранг каждому значению: ранг 1 получает наименьшее значение, а ранг n — наибольшее. Параметр ascending=False меняет направление, поэтому ранг 1 получает наибольшее значение. Результатом является Series с числами с плавающей точкой, а не целыми числами, поскольку по умолчанию одинаковые значения получают среднее арифметическое их рангов. Для столбца из 5 значений ранги находятся в диапазоне от 1.0 до 5.0; при наличии совпадений некоторые значения могут получить общий ранг, например 2.5.

import pandas as pd

s = pd.Series([80, 45, 90, 45, 70])

print('Values:', s.values)
print('Rank (ascending=True, default):', s.rank().values)
print('Rank (ascending=False — best=1):', s.rank(ascending=False).values)
# Note: two 45s share ranks 1 and 2 → both get 1.5

Методы разрешения совпадений в rank()

Параметр method определяет, что происходит со значениями, имеющими одинаковый ранг. Возможные варианты: 'average' (по умолчанию — одинаковые значения получают среднее своих рангов), 'min' (все одинаковые значения получают наименьший ранг), 'max' (все получают наибольший ранг), 'first' (ранги присваиваются в порядке появления — одинаковых рангов нет) и 'dense' (в рангах нет пропусков — последовательность 1, 2, 3, 3, 4 остаётся 1, 2, 3, 3, 4, а не превращается в 1, 2, 3, 3, 5). Метод 'dense' особенно полезен для ранжирования в стиле процентилей.

import pandas as pd

s = pd.Series([80, 45, 90, 45, 70])

result = pd.DataFrame({
    'value': s,
    'average': s.rank(method='average'),
    'min': s.rank(method='min'),
    'max': s.rank(method='max'),
    'first': s.rank(method='first'),
    'dense': s.rank(method='dense')
})
print(result)

Ранжирование внутри групп с помощью groupby().rank()

groupby('group_col')['value_col'].rank() вычисляет ранги независимо внутри каждой группы. В начале каждой группы ранжирование начинается заново: лучший результат в восточном регионе получает ранг 1 в своей группе, и лучший результат в западном регионе также получает ранг 1 в своей группе. Благодаря этому groupby rank так полезен для справедливого сравнения разных групп.

import pandas as pd
import numpy as np

np.random.seed(42)
df = pd.DataFrame({
    'rep': list('ABCDEABCDE'),
    'region': ['East']*5 + ['West']*5,
    'sales': np.random.randint(30000, 100000, 10)
})

# Rank within each region (best = rank 1)
df['rank_in_region'] = df.groupby('region')['sales'].rank(ascending=False, method='min')
df_sorted = df.sort_values(['region', 'rank_in_region'])
print(df_sorted.to_string(index=False))

Ранжирование по процентилям с помощью rank(pct=True)

Если в rank() задать pct=True, функция возвращает процентильный ранг — значение от 0 до 1, показывающее, какая доля значений в группе меньше либо равна текущему значению. Процентильный ранг 0.8 означает, что значение находится на уровне 80-го процентиля — оно выше 80% всех значений. Такая нормализация позволяет напрямую сравнивать значения из групп разного размера, не зная фактическое количество элементов в группах.

import pandas as pd
import numpy as np

np.random.seed(0)
df = pd.DataFrame({
    'name': list('ABCDEFGHIJ'),
    'region': ['East']*5 + ['West']*5,
    'score': np.random.randint(50, 100, 10)
})

# Percentile rank within region
df['pct_rank'] = df.groupby('region')['score'].rank(pct=True)
df['percentile'] = (df['pct_rank'] * 100).round(0).astype(int)
print(df.sort_values(['region', 'percentile'], ascending=[True, False]).to_string(index=False))

pd.qcut: разбиение на интервалы по квантилям

pd.qcut(series, q) делит значения на интервалы с одинаковым количеством наблюдений, поэтому каждый интервал содержит примерно одинаковое число наблюдений. В отличие от pd.cut, который использует интервалы одинаковой ширины, pd.qcut адаптирует границы интервалов к распределению данных. Это удобно для создания квартилей (q=4), квинтилей (q=5) или децилей (q=10) для уровней результативности.

import pandas as pd
import numpy as np

np.random.seed(42)
sales = pd.Series(np.random.exponential(scale=50000, size=100))

# Divide into quartiles
quartiles = pd.qcut(sales, q=4, labels=['Q1', 'Q2', 'Q3', 'Q4'])

result = pd.DataFrame({'sales': sales, 'quartile': quartiles})
print('Quartile counts (should be ~25 each):')
print(result['quartile'].value_counts().sort_index())
print('\nMean sales per quartile:')
print(result.groupby('quartile')['sales'].mean().round(0))

pd.cut и pd.qcut

pd.cut(series, bins=n) создаёт интервалы одинаковой ширины (одинаковый диапазон, разное количество значений). pd.qcut(series, q=n) создаёт интервалы с одинаковым количеством значений (одинаковое количество, разные диапазоны). Для асимметричных данных pd.cut создаёт почти пустые интервалы на хвостах распределения, тогда как pd.qcut равномерно распределяет наблюдения. Выбирайте pd.cut, когда границы интервалов имеют естественный смысл для бизнеса (диапазоны цен, возрастные группы), а pd.qcut — для уровней результативности, где нужны группы одинакового размера.

import pandas as pd
import numpy as np

np.random.seed(0)
# Right-skewed data
data = pd.Series(np.random.exponential(1, 100))

cut_result = pd.cut(data, bins=5).value_counts().sort_index()
qcut_result = pd.qcut(data, q=5).value_counts().sort_index()

print('Equal-width bins (pd.cut) — uneven counts:')
print(cut_result.to_string())
print('\nEqual-frequency bins (pd.qcut) — even counts:')
print(qcut_result.to_string())

Создание меток децилей с помощью pd.qcut

pd.qcut(series, q=10, labels=range(1,11)) назначает каждому наблюдению его дециль (от 1 до 10). Это стандартный приём в маркетинговой аналитике (децили ценности клиентов), кредитном скоринге (децили риска) и тестировании AB (децили трафика для анализа когорт). Параметр labels может принимать любой список той же длины, что и q. Для более понятного результата используйте строки вроде ['Bottom 10%', ..., 'Top 10%'].

import pandas as pd
import numpy as np

np.random.seed(7)
customer_value = pd.Series(np.random.exponential(500, 1000))

# Assign to deciles 1-10
decile_labels = [f'D{i}' for i in range(1, 11)]
customer_decile = pd.qcut(
    customer_value,
    q=10,
    labels=decile_labels
)

result = pd.DataFrame({'value': customer_value, 'decile': customer_decile})
print('Mean customer value per decile:')
print(result.groupby('decile')['value'].mean().round(0))

Процентильные интервалы внутри групп

Объедините groupby и pd.qcut с помощью transform, чтобы создать процентильные интервалы внутри каждой группы. Это полезно, когда нужно ранжировать клиентов внутри каждого региона, а не глобально: клиент, находящийся в нижнем глобальном дециле, может оказаться в верхнем дециле своего региона. Используйте groupby('group')['value'].transform(lambda x: pd.qcut(x, q=4, labels=['Q1','Q2','Q3','Q4'])).

import pandas as pd
import numpy as np

np.random.seed(42)
df = pd.DataFrame({
    'customer': range(20),
    'region': ['North']*10 + ['South']*10,
    'spend': np.random.randint(100, 1000, 20)
})

# Quartile within each region
def quartile_label(x):
    return pd.qcut(x, q=4, labels=['Q1','Q2','Q3','Q4'])

df['region_quartile'] = df.groupby('region')['spend'].transform(quartile_label)
print(df.sort_values(['region', 'region_quartile']).to_string(index=False))

Топ-N внутри групп

Распространённый деловой вопрос: «кто входит в тройку лучших сотрудников каждого региона?» Используйте groupby().rank(), а затем отфильтруйте данные по рангу: df[df['rank_col'] <= 3]. Этот способ корректно работает с группами любого размера и учитывает одинаковые ранги: если на границе есть совпадения, в результат попадёт больше трёх строк. Другой вариант — groupby().nlargest(n), который напрямую возвращает n наибольших значений для каждой группы, не добавляя столбец с рангом.

import pandas as pd
import numpy as np

np.random.seed(0)
df = pd.DataFrame({
    'rep': [f'Rep_{i}' for i in range(15)],
    'region': ['East']*5 + ['West']*5 + ['North']*5,
    'revenue': np.random.randint(40000, 120000, 15)
})

df['region_rank'] = df.groupby('region')['revenue'].rank(ascending=False, method='min')

print('Top 2 per region:')
top2 = df[df['region_rank'] <= 2].sort_values(['region', 'region_rank'])
print(top2[['rep', 'region', 'revenue', 'region_rank']].to_string(index=False))

Объединение ранжирования и transform для нормализации

С помощью groupby().rank(pct=True) вместе с transform можно добавить в исходный DataFrame столбец с процентильным рангом. Такой нормализованный столбец часто полезнее для модели, чем исходное значение: он не зависит от масштаба и позволяет сравнивать группы. В машинном обучении процентильные ранги — простой и более устойчивый к выбросам вариант стандартизации (z-преобразования).

import pandas as pd
import numpy as np

np.random.seed(1)
df = pd.DataFrame({
    'product': np.random.choice(['A', 'B', 'C'], 30),
    'score': np.random.randint(50, 100, 30)
})

# Percentile rank within each product group
df['global_pct'] = df['score'].rank(pct=True).round(3)
df['group_pct'] = df.groupby('product')['score'].rank(pct=True).round(3)

print(df.sort_values(['product', 'score']).head(12).to_string(index=False))

Быстрая проверка

Проверьте, насколько хорошо Вы поняли операции ранжирования и вычисления процентилей из этого урока.

Итоги урока

В этом уроке Вы узнали: Series.rank() вычисляет числовые ранги с настраиваемой обработкой одинаковых значений, groupby().rank() начинает ранжирование заново внутри каждой группы для справедливого сравнения групп, pct=True преобразует ранги в процентили (от 0 до 1), а pd.qcut создаёт интервалы с одинаковым количеством значений для распределения по квартилям, децилям и процентильным уровням. Далее мы рассмотрим способы повысить производительность Pandas: профилирование, отказ от медленных циклов и эффективные типы данных.

Часто задаваемые вопросы

Урок «Ранги и процентили внутри групп» бесплатный?

Да — полный текст урока «Ранги и процентили внутри групп» бесплатно доступен здесь в веб-версии. Чтобы практиковать его интерактивно (встроенный редактор кода и ИИ-репетитор 24/7) и разблокировать остальной курс Pandas & NumPy Academy, подпишись на CoddyKit PRO. Курс Pandas & NumPy Academy содержит 4 уроков всего.

Чему я научусь в уроке «Ранги и процентили внутри групп»?

Вычисляйте ранги внутри групп с помощью groupby().rank() и создавайте интервалы процентилей с помощью pd.qcut для относительного сравнения. Ты практикуешь Pandas & NumPy Academy с помощью реального кода, который запускаешь прямо в браузере, и ИИ-репетитор 24/7 отвечает на твои вопросы во время урока.

Нужен ли мне опыт, чтобы начать Pandas & NumPy Academy?

Предыдущий опыт не требуется. Pandas & NumPy Academy на CoddyKit структурирован для всех уровней — от новичков до продвинутых, поэтому ты можешь начать отсюда или с самого начала и учиться в своем темпе. Это урок 4 из 4.

Сколько времени занимает урок «Ранги и процентили внутри групп»?

Большинство уроков CoddyKit занимают около 5–10 минут. Каждый из них компактный и интерактивный, поэтому ты постоянно делаешь прогресс и продолжаешь с того же места в веб-версии и приложении.

Можно ли писать и запускать код в этом уроке Pandas & NumPy Academy?

Да. Каждый урок Pandas & NumPy Academy включает встроенный редактор кода, поэтому ты пишешь и запускаешь реальный код прямо в браузере и получаешь моментальную обратную связь от AI — локальная установка не требуется.

Все уроки этого курса

  1. Скользящие окна
  2. Расширяющиеся окна
  3. Экспоненциально взвешенное скользящее среднее
  4. Ранги и процентили внутри групп
← Назад к Pandas & NumPy Academy