Точная температура — 17.3 или 18.1 градуса — нужна редко. Обычно хватает «прохладно», «тепло», «жарко». Так же с ценами («дешёвые», «средние», «дорогие»), возрастом, сроками. Превращение чисел в группы по интервалам — частая операция, и для неё есть готовая функция.
pd.cut: свои границы
pd.cut получает столбец и список границ bins. Четыре интервала задают пять границ:
import pandas as pd
weather = pd.read_csv("data/weather.csv", parse_dates=["date"])
moscow = weather[weather["city"] == "Москва"].copy()
pd.cut(moscow["temp_max"], bins=[-50, 0, 15, 25, 50]).head(3)0 (-50, 0] 1 (-50, 0] 2 (-50, 0] Name: temp_max, dtype: category Categories (4, interval[int64, right]): [(-50, 0] < (0, 15] < (15, 25] < (25, 50]]
Вместо температуры в каждой строке теперь интервал. Запись (-50, 0] читается как в математике: круглая
скобка — граница не входит, квадратная — входит. То есть «больше −50 и не больше 0».
Тип результата — category, категория: значение из заранее известного набора. Строка Categories внизу
перечисляет набор, а знаки < показывают, что у категорий есть порядок.
pd.cut(moscow["temp_max"], bins=[-50, 0, 15, 25, 50]).value_counts().sort_index()temp_max (-50, 0] 106 (0, 15] 129 (15, 25] 121 (25, 50] 9 Name: count, dtype: int64
Порядок пригодился: sort_index расставил интервалы по шкале — от холодных к тёплым.
Интервалам можно дать названия — параметром labels. Названий должно быть на одно меньше, чем границ:
names = ["ниже нуля", "прохладно", "тепло", "жарко"]
moscow["kind"] = pd.cut(moscow["temp_max"], bins=[-50, 0, 15, 25, 50], labels=names)
moscow[["date", "temp_max", "kind"]].head(3)| date | temp_max | kind | |
|---|---|---|---|
| 0 | 2025-01-01 | -3.2 | ниже нуля |
| 1 | 2025-01-02 | -3.2 | ниже нуля |
| 2 | 2025-01-03 | -3.7 | ниже нуля |
moscow["kind"].value_counts().sort_index()kind ниже нуля 106 прохладно 129 тепло 121 жарко 9 Name: count, dtype: int64
Добавьте в таблицу sochi столбец kind — тип дня по temp_max с теми же границами и подписями, что для
Москвы: до 0 — «ниже нуля», до 15 — «прохладно», до 25 — «тепло», выше — «жарко». В kinds сохраните число
дней каждого типа по порядку шкалы.
pd.cut(sochi["temp_max"], bins=[-50, 0, 15, 25, 50], labels=[...]) — четыре подписи.sochi["kind"].value_counts().sort_index().Заготовка упражнения обновилась с тех пор, как вы сохранили свой код.
Вернуть заготовку? Ваш код в этом упражнении будет удалён.
sochi = weather[weather["city"] == "Сочи"].copy()
# добавьте в sochi столбец kind
kinds = ...Упражнение пока не решено, поэтому для ячеек ниже выполнено эталонное решение — ваш код не изменён.
sochi = weather[weather["city"] == "Сочи"].copy()
sochi["kind"] = pd.cut(sochi["temp_max"], bins=[-50, 0, 15, 25, 50], labels=["ниже нуля", "прохладно", "тепло", "жарко"])
kinds = sochi["kind"].value_counts().sort_index()Дней «ниже нуля» в Сочи нет, но строка с нулём в ответе осталась: категория помнит весь набор значений, даже те, что не встретились.
Границы
Куда попадёт значение, равное границе? По умолчанию — в интервал слева от неё: правая граница входит.
marks = pd.Series([0, 15, 15.1, 25])
print(pd.cut(marks, bins=[0, 15, 25]).tolist())[nan, Interval(0.0, 15.0, closed='right'), Interval(15.0, 25.0, closed='right'), Interval(15.0, 25.0, closed='right')]
В списке интервалы показаны подробной записью: closed='right' значит «закрыт справа», то есть (0, 15].
Ровно 15 — это ещё (0, 15], а 15.1 — уже (15, 25]. Ноль не попал никуда: левая граница первого интервала
не входит, и вместо интервала стоит nan.
Параметр right=False переворачивает правило: входит левая граница, правая — нет.
print(pd.cut(marks, bins=[0, 15, 25], right=False).tolist())[Interval(0.0, 15.0, closed='left'), Interval(15.0, 25.0, closed='left'), Interval(15.0, 25.0, closed='left'), nan]
Значения за пределами границ тоже становятся пропусками — без ошибки и предупреждения:
ages = pd.Series([5, 15, 30, 60])
pd.cut(ages, bins=[0, 15, 30], labels=["дети", "молодые"])0 дети 1 дети 2 молодые 3 NaN dtype: category Categories (2, str): ['дети' < 'молодые']
Какую подпись получит значение 10:
pd.cut(pd.Series([10]), bins=[0, 10, 20], labels=["низкий", "высокий"])?
(0, 10] и (10, 20]. Правая граница входит, поэтому 10 относится к первому — «низкий». С right=False интервалы стали бы [0, 10) и [10, 20), и 10 попало бы в «высокий».Добавьте в delivery столбец speed по сроку days: до 3 дней включительно — «быстро», 4–5 дней —
«нормально», 6 дней и больше — «долго». Самый долгий срок в данных — 13 дней. В speed_share сохраните
доли каждой группы по порядку шкалы.
[0, 3, 5, 14]: интервалы (0, 3], (3, 5], (5, 14].delivery["speed"].value_counts(normalize=True).sort_index().Заготовка упражнения обновилась с тех пор, как вы сохранили свой код.
Вернуть заготовку? Ваш код в этом упражнении будет удалён.
delivery = pd.read_csv("data/delivery_h1.csv")
# добавьте в delivery столбец speed
speed_share = ...Упражнение пока не решено, поэтому для ячеек ниже выполнено эталонное решение — ваш код не изменён.
delivery = pd.read_csv("data/delivery_h1.csv")
delivery["speed"] = pd.cut(delivery["days"], bins=[0, 3, 5, 14], labels=["быстро", "нормально", "долго"])
speed_share = delivery["speed"].value_counts(normalize=True).sort_index()Пропуски в days остались пропусками в speed — их 36, и в доли они не вошли.
Равные интервалы и равные группы
Если вместо списка границ передать число, pd.cut сам разделит диапазон на столько интервалов равной
ширины:
pd.cut(moscow["wind_ms"], bins=3).value_counts().sort_index()wind_ms (0.487, 4.867] 272 (4.867, 9.233] 81 (9.233, 13.6] 12 Name: count, dtype: int64
Ширина одинаковая, а строк в интервалах — очень разное число: сильный ветер бывает редко. Левая граница первого интервала (0.487) чуть меньше наименьшего значения (0.5) — pandas сдвигает её, чтобы минимум тоже попал внутрь: ведь левая граница не входит.
Функция pd.qcut делит иначе — по квантилям: так, чтобы в каждой группе оказалось примерно поровну строк.
Границы она подбирает сама:
orders = pd.read_csv("data/shop_orders.csv")
revenue = orders["price"] * orders["quantity"]
pd.qcut(revenue, 4).value_counts().sort_index()(149.999, 535.0] 612 (535.0, 1080.0] 665 (1080.0, 1580.0] 598 (1580.0, 9030.0] 573 Name: count, dtype: int64
Границы — знакомые квартили: 535, 1080 и 1580 ₽. Группы получились не совсем равными, потому что многие строки имеют одинаковую выручку, а делить одинаковые значения между группами нельзя.
Когда что выбирать: pd.cut — когда границы имеют смысл сами по себе (ноль градусов, 1000 рублей,
совершеннолетие); pd.qcut — когда нужны группы «нижняя четверть, верхняя четверть» относительно самих
данных.
Добавьте в orders столбец size — четверть, в которую попадает выручка строки revenue: «малая», «средняя»,
«крупная», «очень крупная». В big сохраните строки верхней четверти, а в big_min — наименьшую выручку среди них.
pd.qcut(revenue, 4, labels=["малая", "средняя", "крупная", "очень крупная"]).big = orders[orders["size"] == "очень крупная"]; выручка строк — big["price"] * big["quantity"].Заготовка упражнения обновилась с тех пор, как вы сохранили свой код.
Вернуть заготовку? Ваш код в этом упражнении будет удалён.
# добавьте в orders столбец size
big = ...
big_min = ...Упражнение пока не решено, поэтому для ячеек ниже выполнено эталонное решение — ваш код не изменён.
orders["size"] = pd.qcut(revenue, 4, labels=["малая", "средняя", "крупная", "очень крупная"])
big = orders[orders["size"] == "очень крупная"]
big_min = (big["price"] * big["quantity"]).min()Итоги
pd.cut(столбец, bins=[...], labels=[...])— группы по своим границам; подписей на одну меньше, чем границ.(a, b]— правая граница входит;right=False— входит левая.- Значения за границами становятся
NaN: берите границы с запасом. pd.cut(столбец, bins=n)—nинтервалов равной ширины;pd.qcut(столбец, n)—nгрупп с примерно равным числом строк.