Shanraq.org Shanraq.org
DataFrame и Series: подписи вместо номеров
IT

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: по чему подписаны строки. Половина странностей в чужом коде объясняется тем, что там индекс, о котором забыли.

Карта урока

Карта урока: подписи вместо номеров

Скажите своими словами

Не подглядывая, ответьте вслух или на бумаге. Ответы — в конце урока.

  1. Чем loc отличается от iloc и почему у них разные правила для среза?
  2. Что вернёт df["KZ"] и чем это отличается от списка чисел?
  3. Откуда в разности двух рядов взялся 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())

Задание

Обязательное. Возьмите ту же таблицу инфляции из урока. Ответьте на три вопроса, каждый — обращением к подписям, а не циклом:

  1. В каком году был пик инфляции в каждой стране и сколько он составил (idxmax возвращает подпись, то есть год).
  2. Во сколько раз выросли цены за четыре года в каждой стране: произведение (1 + i/100) по столбцу.
  3. Какой год был самым спокойным по среднему трёх стран (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 — следующий урок.

Долги. Индекс у сводки — год числом, а не датой; для месячного ряда этого уже не хватит, и в уроке про время в таблице придётся переходить на настоящие даты.

Ответы

Показать ответы

На вопросы

  1. loc спрашивает по подписи, iloc — по номеру. Правила среза разные, потому что вопросы разные: «с 2022 по 2024» на человеческом языке включает 2024, а срез по номерам — это обычный питоновский срез, где конец не входит.
  2. Series — столбец: значения плюс подписи. От списка он отличается тем, что каждое число знает свой год, и это знание переживает сортировку, фильтр и арифметику.
  3. Из несовпадения подписей. У второго ряда нет 2021 и 2022, поэтому для этих лет разности не существует, и pandas ставит NaN вместо того, чтобы молча выкинуть строки или сдвинуть числа.

К разминке

  1. Обращение по подписи даёт число, index — список подписей, а сравнение отбирает строки, где условие верно: rates > 7 даёт ряд из «да/нет», и им же таблица фильтруется.
10.8
['KZ', 'UZ', 'RU'] | среднее: 8.5
KZ     8.0
UZ    10.8
dtype: float64
  1. 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]
  1. 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() возвращает подпись, а не позицию, поэтому год печатается сам собой. Если бы индекс не задали, вернулся бы номер строки — и пришлось бы вспоминать, какой это год.

Источники

Если вы нашли ошибку или опечатку в тексте статьи, то сообщите нам об этом

Проверить задание

Сначала решите и запустите в VS Code — редактор покажет ошибку на месте. Готовое решение вставьте сюда. Проверяет модель: она укажет на ошибку, но не даст готовый ответ.

Чтобы проверить, нужно войти. Войти

Комментарии (0)

Пока нет комментариев. Будьте первым.