Python: от данных до своей сводки Урок 26 из 56
DataFrame и Series: подписи вместо номеров
Двадцать пятый урок курса по Python. Таблица, у которой есть подписи строк, отвечает не так, как список: `loc` берёт по подписи, `iloc` — по номеру, а сложение двух рядов идёт по годам, а не по порядку. Разбираем `DataFrame` и `Series`, индекс, срезы и `NaN`, который появляется сам.
Зачем это нужно
Прошлый урок закончился правилом: таблица и больше одного вопроса к ней — берите pandas. Этот урок про то, из чего эта таблица сделана.
Главное отличие от списка — подписи. У списка есть только номера: третий элемент, седьмой. У таблицы есть имена столбцов и подписи строк, и все ответы приходят с ними. Год максимума возвращается годом, а не позицией. Два ряда складываются по годам, даже если один короче. Именно это, а не скорость, меняет то, как пишется программа.
Сразу целиком
Файл tablica.py. Инфляция в трёх странах за четыре года — данные Всемирного банка, округлены до десятых.
"""Урок 25: таблица как таблица.
Инфляция в трёх странах за четыре года. Один и тот же ряд лежит в таблице
столбцами, у строк есть подписи, и от подписей зависит всё остальное.
"""
import pandas as pd
# Инфляция, % за год. Данные Всемирного банка, округлены до десятых.
data = {
"KZ": [8.0, 15.0, 14.5, 8.7],
"UZ": [10.8, 11.4, 10.0, 9.6],
"RU": [6.7, 13.7, 5.9, 8.4],
}
df = pd.DataFrame(data, index=[2021, 2022, 2023, 2024])
df.index.name = "год"
print(df)
print()
print("== из чего она состоит")
print("форма:", df.shape, "| столбцы:", list(df.columns))
print("подписи строк:", df.index.tolist())
print(df.dtypes)
print()
print("== столбец — это Series")
kz = df["KZ"]
print("тип:", type(kz).__name__, "| подписи:", kz.index.tolist())
print(kz)
print()
print("== строка по подписи и строка по номеру")
print("loc[2022]:", df.loc[2022].to_dict())
print("iloc[1]: ", df.iloc[1].to_dict())
print("одна ячейка:", df.loc[2023, "UZ"])
print("loc[2022:2024] годы:", df.loc[2022:2024].index.tolist(), "— конец включён")
print("iloc[1:3] годы: ", df.iloc[1:3].index.tolist(), "— конец не включён")
print()
print("== новый столбец считается из старых")
df["среднее"] = df.mean(axis=1).round(2)
print(df)
print()
print("== сложение идёт по подписям, а не по порядку")
part = pd.Series([1.0, 2.0], index=[2023, 2024])
print(df["KZ"] - part)
Выводит:
KZ UZ RU
год
2021 8.0 10.8 6.7
2022 15.0 11.4 13.7
2023 14.5 10.0 5.9
2024 8.7 9.6 8.4
== из чего она состоит
форма: (4, 3) | столбцы: ['KZ', 'UZ', 'RU']
подписи строк: [2021, 2022, 2023, 2024]
KZ float64
UZ float64
RU float64
dtype: object
== столбец — это Series
тип: Series | подписи: [2021, 2022, 2023, 2024]
год
2021 8.0
2022 15.0
2023 14.5
2024 8.7
Name: KZ, dtype: float64
== строка по подписи и строка по номеру
loc[2022]: {'KZ': 15.0, 'UZ': 11.4, 'RU': 13.7}
iloc[1]: {'KZ': 15.0, 'UZ': 11.4, 'RU': 13.7}
одна ячейка: 10.0
loc[2022:2024] годы: [2022, 2023, 2024] — конец включён
iloc[1:3] годы: [2022, 2023] — конец не включён
== новый столбец считается из старых
KZ UZ RU среднее
год
2021 8.0 10.8 6.7 8.50
2022 15.0 11.4 13.7 13.37
2023 14.5 10.0 5.9 10.13
2024 8.7 9.6 8.4 8.90
== сложение идёт по подписям, а не по порядку
год
2021 NaN
2022 NaN
2023 13.5
2024 6.7
dtype: float64
Разбор
Два предмета, а не один
DataFrame — таблица: столбцы с именами и строки с подписями. Series — один столбец: значения плюс те же подписи. Всё остальное следует из этой пары.
df["KZ"] возвращает Series, и подписи у него те же, что у таблицы: годы. Не список чисел, за которым надо помнить, какой год где, а числа вместе с годами. Поэтому kz.index.tolist() печатает [2021, 2022, 2023, 2024], а не [0, 1, 2, 3].
df.dtypes — тоже Series: имена столбцов слева, их типы справа. Отсюда и dtype: object в последней строке этого вывода: сами типы — не числа, а объекты, и столбец из них имеет тип «что угодно».
Индекс — это подписи, а не порядок
Мы задали index=[2021, 2022, 2023, 2024], и с этого момента строка называется годом. Не «нулевая строка», а «две тысячи двадцать первый». Индекс можно назвать (df.index.name = "год"), и тогда таблица печатается с этим именем в углу.
Из этого следует разница, на которой спотыкаются все:
df.loc[2022]— строка по подписи. Двадцать второй год.df.iloc[1]— строка по номеру. Вторая по счёту.
В нашей таблице это одна и та же строка, и именно поэтому пример коварен: пока подписи идут подряд от нуля, разницы не видно. Она появляется, как только строки отсортировали или отфильтровали.
Второе следствие — срезы. df.loc[2022:2024] даёт три года: у подписей конец включён, потому что «с 2022 по 2024» на человеческом языке значит именно это. df.iloc[1:3] даёт два: у номеров конец не включён, как у обычного среза списка из пятого урока. Одна и та же запись, два разных правила — потому что вопросы разные.
Ячейка берётся одним обращением: df.loc[2023, "UZ"] — сначала подпись строки, потом имя столбца.
Новый столбец — это выражение, а не цикл
df["среднее"] = df.mean(axis=1).round(2) добавляет столбец. axis=1 значит «поперёк строки»: посчитать по трём странам для каждого года. Без axis считалось бы по столбцам — среднее за все годы для каждой страны, и это другой вопрос.
Столбец с новым именем создаётся присваиванием, столбец с существующим — заменяется. Это единственное место, где df["имя"] = ... ведёт себя как словарь.
Подписи складывают, а не выстраивают в ряд
Вот главное, ради чего урок:
part = pd.Series([1.0, 2.0], index=[2023, 2024])
df["KZ"] - part
Результат — четыре строки, из них две NaN. pandas не вычитает «первое из первого»: он сопоставляет подписи. У part нет 2021 и 2022 — значит, для этих лет ответа нет, и в них стоит NaN, «нет числа».
Сравните с восьмым уроком, где zip соединял два ряда по позициям, а не по годам: короткий ряд молча обрезал длинный, и ошибка выглядела как правильный ответ. Здесь наоборот — несовпадение подписей видно сразу, потому что оно напечатано.
NaN — не ноль и не пропуск в вашем коде: это значение, которое означает «здесь числа нет». Оно заразно в арифметике (NaN + 5 даёт NaN) и невидимо для mean(), который его просто пропускает. Что с ним делать — тема урока про грязные данные, а пока достаточно знать, откуда он берётся: из подписей, которые не совпали.
Что посмотреть первым делом
Когда таблица приходит от кого-то другого, первые три вопроса всегда одни и те же:
df.shape— сколько строк и столбцов;df.dtypes— чем pandas считает каждый столбец (число? строка? дата?);df.head(3)— как оно выглядит.
Четвёртый вопрос — df.index: по чему подписаны строки. Половина странностей в чужом коде объясняется тем, что там индекс, о котором забыли.
Карта урока
Скажите своими словами
Не подглядывая, ответьте вслух или на бумаге. Ответы — в конце урока.
- Чем
locотличается отilocи почему у них разные правила для среза? - Что вернёт
df["KZ"]и чем это отличается от списка чисел? - Откуда в разности двух рядов взялся
NaN, если оба ряда — числа?
Разминка
Три коротких шага перед заданием: предсказать, дописать, починить. Ответы — в конце урока, но сначала ответьте сами.
1. Предскажите. Что напечатает эта программа?
import pandas as pd
rates = pd.Series({"KZ": 8.0, "UZ": 10.8, "RU": 6.7})
print(rates["UZ"])
print(rates.index.tolist(), "| среднее:", round(rates.mean(), 2))
print(rates[rates > 7])
2. Заполните пропуск. Вместо ... поставьте подписи так, чтобы разность посчиталась для обоих лет, а не дала NaN.
# подписи двух рядов должны совпасть, иначе вычитать нечего
import pandas as pd
a = pd.Series([14.5, 8.7], index=[2023, 2024])
b = pd.Series([10.0, 9.6], index=[...])
print((a - b).round(1).tolist())
3. Почините. Программа берёт строку 2022 года и падает с KeyError: 2022. Замените одно обращение.
# квадратные скобки у таблицы спрашивают про столбец, а нам нужна строка
import pandas as pd
df = pd.DataFrame({"KZ": [15.0, 14.5], "UZ": [11.4, 10.0]}, index=[2022, 2023])
print(df[2022].to_dict())
Задание
Обязательное. Возьмите ту же таблицу инфляции из урока. Ответьте на три вопроса, каждый — обращением к подписям, а не циклом:
- В каком году был пик инфляции в каждой стране и сколько он составил (
idxmaxвозвращает подпись, то есть год). - Во сколько раз выросли цены за четыре года в каждой стране: произведение
(1 + i/100)по столбцу. - Какой год был самым спокойным по среднему трёх стран (
idxmin).
Ожидаемый вывод:
таблица:
KZ UZ RU
год
2021 8.0 10.8 6.7
2022 15.0 11.4 13.7
2023 14.5 10.0 5.9
2024 8.7 9.6 8.4
пик по странам:
KZ: 2022 (15.0)
UZ: 2022 (11.4)
RU: 2022 (13.7)
цены за четыре года выросли:
KZ: в 1.55 раза
UZ: в 1.49 раза
RU: в 1.39 раза
самый спокойный год: 2021 (среднее 8.50)
Готово, когда: вывод совпадает построчно; год пика получен из idxmax, а не поиском в цикле; рост цен посчитан произведением, а не суммой процентов; средний год найден по среднему трёх стран, а не по одной.
На своих данных. Возьмите любой свой ряд по годам или месяцам — расходы, продажи, курс, что угодно — и постройте из него Series с осмысленным индексом. Найдите пик и провал через idxmax и idxmin. Проверьте, что ответ вернулся подписью, а не номером: если номером, значит индекс вы не задали.
По желанию.
- Поменяйте порядок строк (
df.sort_values("KZ")) и посмотрите, как после этого расходятсяloc[2022]иiloc[1]. - Постройте ту же таблицу из списка словарей (
pd.DataFrame([{...}, {...}])) и сравните, что получилось с индексом. - Сделайте
df.Tи объясните себе, чем стали столбцы и подписи.
Куда это встанет в проекте
Седьмой шаг: сводка перестаёт держать ряд словарём. sholu/esep.py строит из него DataFrame с годами в подписях, и три цикла исчезают:
- среднее, которое приходилось учить пропускать годы без числа, —
mean()пропускает их сам; - подсчёт пропусков —
isna().sum(); - поиск максимума, который тащил за собой год, —
idxmax()и есть год.
Диск пока не тронут: CSV по-прежнему пишет и читает модуль csv. read_csv и to_csv — следующий урок.
Долги. Индекс у сводки — год числом, а не датой; для месячного ряда этого уже не хватит, и в уроке про время в таблице придётся переходить на настоящие даты.
Ответы
Показать ответы
На вопросы
locспрашивает по подписи,iloc— по номеру. Правила среза разные, потому что вопросы разные: «с 2022 по 2024» на человеческом языке включает 2024, а срез по номерам — это обычный питоновский срез, где конец не входит.Series— столбец: значения плюс подписи. От списка он отличается тем, что каждое число знает свой год, и это знание переживает сортировку, фильтр и арифметику.- Из несовпадения подписей. У второго ряда нет 2021 и 2022, поэтому для этих лет разности не существует, и pandas ставит
NaNвместо того, чтобы молча выкинуть строки или сдвинуть числа.
К разминке
- Обращение по подписи даёт число,
index— список подписей, а сравнение отбирает строки, где условие верно:rates > 7даёт ряд из «да/нет», и им же таблица фильтруется.
10.8
['KZ', 'UZ', 'RU'] | среднее: 8.5
KZ 8.0
UZ 10.8
dtype: float64
index=[2023, 2024]. Числа сами по себе ничего не значат: вычитается 2023-й из 2023-го, а не первое из первого.
import pandas as pd
a = pd.Series([14.5, 8.7], index=[2023, 2024])
b = pd.Series([10.0, 9.6], index=[2023, 2024])
print((a - b).round(1).tolist())
[4.5, -0.9]
df[2022]спрашивает про столбец с именем2022, а такого столбца нет — отсюдаKeyError. Строка берётся черезdf.loc[2022].
import pandas as pd
df = pd.DataFrame({"KZ": [15.0, 14.5], "UZ": [11.4, 10.0]}, index=[2022, 2023])
print(df.loc[2022].to_dict())
{'KZ': 15.0, 'UZ': 11.4}
Правило, которое стоит запомнить: квадратные скобки у таблицы — это про столбцы, loc и iloc — про строки.
К заданию
Рост цен — произведение, а не сумма: инфляция в 8 % и 15 % подряд даёт не 23 %, а 1.08 × 1.15 = 1.242, то есть 24,2 %. В pandas это (1 + df / 100).prod() — деление и сложение применяются ко всей таблице сразу, а prod() перемножает по столбцу.
idxmax() возвращает подпись, а не позицию, поэтому год печатается сам собой. Если бы индекс не задали, вернулся бы номер строки — и пришлось бы вспоминать, какой это год.
Источники
- Введение в структуры данных pandas —
SeriesиDataFrameиз первых рук. - Индексирование и выбор данных — раздел про
locиiloc, включая правило о концах срезов. - Инфляция потребительских цен, Всемирный банк — источник чисел этого урока.
Если вы нашли ошибку или опечатку в тексте статьи, то сообщите нам об этом
Комментарии (0)
Войдите, чтобы оставить комментарий →
Пока нет комментариев. Будьте первым.