Нашли ошибку?
Скачать чистый
урок 2 из 3≈ 20 минупражнения: 0 из 3

Выравнивание по индексу

Арифметика двух Series идёт не по порядку, а по меткам: одинаковые метки находят друг друга, а несовпавшие дают NaN. Методы add и sub с fill_value, reindex, сравнение Series и запись Series в столбец таблицы.

Python запустится при первом выполнении

Самое важное, что делает индекс, происходит незаметно. Когда вы складываете или вычитаете два Series, pandas не берёт «первое значение с первым, второе со вторым». Он сопоставляет значения по меткам. Это называется выравниванием (alignment). Оно объясняет и удобства pandas, и самые загадочные его пропуски.

По меткам, а не по порядку

Остатки на складе и продажи за день. Порядок напитков в двух Series разный:

[ ]Порядок разный, результат верный
import pandas as pd

stock = pd.Series({"кофе": 40, "чай": 25, "какао": 10})
sold = pd.Series({"чай": 5, "кофе": 12, "какао": 4})
stock - sold
Выводсохранённый · нажмите «Выполнить», чтобы получить свой
какао     6
кофе     28
чай      20
dtype: int64

Из кофе вычтен кофе, из чая — чай, хотя стоят они на разных местах. Если бы значения сопоставлялись по порядку, как в цикле по двум спискам, получилось бы «кофе минус чай». Метки результата идут по алфавиту: когда индексы двух Series различаются, pandas объединяет их и сортирует.

Теперь метки совпадают не полностью: какао сегодня не покупали, зато продали сироп, которого нет в остатках.

[ ]Нет пары — NaN
sold = pd.Series({"чай": 5, "кофе": 12, "сироп": 3})
stock - sold
Выводсохранённый · нажмите «Выполнить», чтобы получить свой
какао     NaN
кофе     28.0
сироп     NaN
чай      20.0
dtype: float64

В результат попали все метки из обоих Series. Там, где пара нашлась, — разность. Там, где метка есть только с одной стороны, — NaN: pandas не знает, что вычитать из отсутствующего значения.

Это правило общее для всей арифметики между Series: +, -, *, /.

fill_value: считать отсутствующее нулём

Часто по смыслу «нет значения» — это ноль: какао не продавали, значит, продано ноль. У арифметических операторов есть методы-двойники — add, sub, mul, div — с параметром fill_value: чем заменить отсутствующее значение перед вычислением.

[ ]Вычитание с нулём вместо отсутствующего
stock.sub(sold, fill_value=0)
Выводсохранённый · нажмите «Выполнить», чтобы получить свой
какао    10.0
кофе     28.0
сироп    -3.0
чай      20.0
dtype: float64

Какао: 10 − 0 = 10. Сироп: 0 − 3 = −3 — продали то, чего не было на складе; это уже повод проверить учёт.

Вопрос
x = pd.Series([1, 2], index=["a", "b"])
y = pd.Series([10, 20], index=["b", "c"])
print((x + y).isna().sum())

Что выведет этот код?

[ ]Упражнение 1Поставка решено

В start — остатки напитков на начало дня, в delivered — что привезли. Сохраните в wrong_total результат простого сложения start + delivered, а в total — правильный остаток после поставки: без пропусков.

start = pd.Series({"эспрессо": 50, "латте": 30, "какао": 12, "чай": 20})
delivered = pd.Series({"латте": 10, "чай": 15, "раф": 8})
wrong_total = ...
total = ...

План и факт

Настоящая задача. Выручка по городам из таблицы продаж — и годовой план из файла data/plan.csv:

[ ]Факт и план по городам
orders = pd.read_csv("data/shop_orders.csv")
orders["revenue"] = orders["price"] * orders["quantity"]
fact = orders.groupby("city")["revenue"].sum()
plan_table = pd.read_csv("data/plan.csv")
plan = plan_table.groupby("city")["plan"].sum()
print(fact)
print(plan)
Выводсохранённый · нажмите «Выполнить», чтобы получить свой
city
Екатеринбург        423350
Казань              440150
Москва             1310110
Новосибирск         324090
Санкт-Петербург     803720
Name: revenue, dtype: int64
city
Екатеринбург        398000
Казань              451000
Москва             1313000
Новосибирск         329000
Санкт-Петербург     841000
Сочи                 60000
Name: plan, dtype: int64

В плане шесть городов: магазин собирался выйти в Сочи, но не вышел. В факте — пять.

[ ]Факт минус план
fact - plan
Выводсохранённый · нажмите «Выполнить», чтобы получить свой
city
Екатеринбург       25350.0
Казань            -10850.0
Москва             -2890.0
Новосибирск        -4910.0
Санкт-Петербург   -37280.0
Сочи                   NaN
dtype: float64

У пяти городов — разность, у Сочи — NaN, и весь результат стал дробным. Заметьте: Сочи не пропал, он есть в результате. Выравнивание не теряет метки — оно честно показывает, где данных не хватило.

reindex: привести к нужным меткам

Метод reindex перестраивает Series под заданный список меток: существующие значения остаются, для новых меток ставится NaN, лишние отбрасываются.

[ ]Три города в заданном порядке — с пропуском и с нулём
print(fact.reindex(["Москва", "Сочи", "Казань"]))
print(fact.reindex(["Москва", "Сочи", "Казань"], fill_value=0))
Выводсохранённый · нажмите «Выполнить», чтобы получить свой
city
Москва    1310110.0
Сочи            NaN
Казань     440150.0
Name: revenue, dtype: float64
city
Москва    1310110
Сочи            0
Казань     440150
Name: revenue, dtype: int64

С fill_value новые метки получают указанное значение вместо NaN.

Набор меток можно взять у другого Series — его атрибут index. Оставим в плане только города, где были продажи:

[ ]План по городам из факта
plan.reindex(fact.index)
Выводсохранённый · нажмите «Выполнить», чтобы получить свой
city
Екатеринбург        398000
Казань              451000
Москва             1313000
Новосибирск         329000
Санкт-Петербург     841000
Name: plan, dtype: int64

Сочи пропал: в fact.index его нет.

[ ]Упражнение 2Выполнение плана решено

Сохраните в fact_full факт по всем городам из плана (где продаж не было — ноль). В gap — отклонение: факт минус план. В done — долю выполнения плана: факт, делённый на план, с округлением до трёх знаков. В best_city — город с наибольшей долей выполнения.

fact_full = ...
gap = ...
done = ...
best_city = ...

Сравнения строже арифметики

Арифметика несовпадающие метки прощает. Сравнение — нет:

[ ]Сравнение Series с разными меткамиошибка: ValueError
fact > plan
Ошибкасохранённый · нажмите «Выполнить», чтобы получить свой
ValueError: Can only compare identically-labeled Series objects

Ошибка разумна: на вопрос «больше ли факт плана в Сочи» нет ответа, пока неизвестен факт. Для сравнения метки должны совпадать полностью — и набор, и порядок. Сначала приводим оба Series к одним меткам, потом сравниваем:

[ ]После reindex
fact.reindex(plan.index, fill_value=0) >= plan
Выводсохранённый · нажмите «Выполнить», чтобы получить свой
city
Екатеринбург        True
Казань             False
Москва             False
Новосибирск        False
Санкт-Петербург    False
Сочи               False
dtype: bool

Series в столбец таблицы

Выравнивание работает и при записи Series в столбец: значения расставляются по меткам индекса таблицы.

[ ]Метки не совпали
cities = pd.DataFrame({"city": ["Москва", "Казань", "Сочи"]})
cities["fact"] = fact
cities
Выводсохранённый · нажмите «Выполнить», чтобы получить свой
city fact
0 Москва NaN
1 Казань NaN
2 Сочи NaN

В таблице cities метки — 0, 1, 2, а у fact — названия городов. Ни одна не совпала, и весь столбец — NaN. Это та же ошибка, что orders["city_total"] = orders.groupby("city")["revenue"].sum() из урока о transform. Значение по ключу из другого столбца подставляет map. До сих пор мы передавали ему словарь; Series он принимает так же — ищет значение столбца среди его меток, как ключ в словаре:

[ ]map ищет по значению столбца city
cities["fact"] = cities["city"].map(fact)
cities
Выводсохранённый · нажмите «Выполнить», чтобы получить свой
city fact
0 Москва 1310110.0
1 Казань 440150.0
2 Сочи NaN

Два уровня

Выравнивание работает с любым индексом — в том числе из двух уровней. План в файле задан по городам и месяцам. Сделаем из него Series с таким же двухуровневым индексом, как у результата группировки: set_index принимает список столбцов.

[ ]Факт и план по парам «город, месяц»
orders["month"] = pd.to_datetime(orders["date"]).dt.month
fact_m = orders.groupby(["city", "month"])["revenue"].sum()
plan_m = plan_table.set_index(["city", "month"])["plan"]
print(len(fact_m), len(plan_m))
plan_m.head(3)
Выводсохранённый · нажмите «Выполнить», чтобы получить свой
60 61
city    month
Москва  1        132000
        2        126000
        3        108000
Name: plan, dtype: int64

В факте 60 пар (5 городов × 12 месяцев), в плане 61: у Екатеринбурга плана нет за январь и февраль, зато есть три месяца Сочи.

[ ]Пять пар без пары
diff = fact_m - plan_m
print(len(diff), diff.isna().sum())
diff.loc["Екатеринбург"].head(4)
Выводсохранённый · нажмите «Выполнить», чтобы получить свой
63 5
month
1       NaN
2       NaN
3   -1510.0
4   -2210.0
dtype: float64

Объединённых пар 63; у пяти из них нет либо плана, либо факта.

[ ]Упражнение 3Отклонение по месяцам решено

Сохраните в gap_m отклонение факта от плана по парам «город, месяц»; отсутствующий план или факт считайте нулём. В kazan_march сохраните отклонение Казани в марте. В no_plan — сумму отклонений Екатеринбурга за январь и февраль (месяцы, на которые плана не было).

gap_m = ...
kazan_march = ...
no_plan = ...

Итоги

  • Арифметика двух Series идёт по меткам индекса; порядок строк не важен.
  • Метка без пары даёт NaN; в результате остаются все метки обоих операндов.
  • a.sub(b, fill_value=0) (и add, mul, div) считает отсутствующее значение нулём.
  • reindex(метки, fill_value=...) приводит Series к нужному набору меток.
  • Сравнение требует одинаковых меток в одинаковом порядке; запись Series в столбец тоже выравнивается по индексу.

Подробнее в справочнике

Нашли ошибку?