Самое важное, что делает индекс, происходит незаметно. Когда вы складываете или вычитаете два 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 объединяет их и сортирует.
Теперь метки совпадают не полностью: какао сегодня не покупали, зато продали сироп, которого нет в остатках.
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())Что выведет этот код?
b (2 + 10 = 12). У a и c пары нет — два пропуска.В start — остатки напитков на начало дня, в delivered — что привезли. Сохраните в wrong_total результат
простого сложения start + delivered, а в total — правильный остаток после поставки: без пропусков.
start.add(delivered, fill_value=0).Заготовка упражнения обновилась с тех пор, как вы сохранили свой код.
Вернуть заготовку? Ваш код в этом упражнении будет удалён.
start = pd.Series({"эспрессо": 50, "латте": 30, "какао": 12, "чай": 20})
delivered = pd.Series({"латте": 10, "чай": 15, "раф": 8})
wrong_total = ...
total = ...Упражнение пока не решено, поэтому для ячеек ниже выполнено эталонное решение — ваш код не изменён.
start = pd.Series({"эспрессо": 50, "латте": 30, "какао": 12, "чай": 20})
delivered = pd.Series({"латте": 10, "чай": 15, "раф": 8})
wrong_total = start + delivered
total = start.add(delivered, fill_value=0)План и факт
Настоящая задача. Выручка по городам из таблицы продаж — и годовой план из файла 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 - plancity Екатеринбург 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 его нет.
Сохраните в fact_full факт по всем городам из плана (где продаж не было — ноль). В gap — отклонение: факт
минус план. В done — долю выполнения плана: факт, делённый на план, с округлением до трёх знаков.
В best_city — город с наибольшей долей выполнения.
fact.reindex(plan.index, fill_value=0).fact_full - plan, (fact_full / plan).round(3), done.idxmax().Заготовка упражнения обновилась с тех пор, как вы сохранили свой код.
Вернуть заготовку? Ваш код в этом упражнении будет удалён.
fact_full = ...
gap = ...
done = ...
best_city = ...Упражнение пока не решено, поэтому для ячеек ниже выполнено эталонное решение — ваш код не изменён.
fact_full = fact.reindex(plan.index, fill_value=0)
gap = fact_full - plan
done = (fact_full / plan).round(3)
best_city = done.idxmax()Сравнения строже арифметики
Арифметика несовпадающие метки прощает. Сравнение — нет:
fact > planValueError: Can only compare identically-labeled Series objects
Ошибка разумна: на вопрос «больше ли факт плана в Сочи» нет ответа, пока неизвестен факт. Для сравнения метки должны совпадать полностью — и набор, и порядок. Сначала приводим оба Series к одним меткам, потом сравниваем:
fact.reindex(plan.index, fill_value=0) >= plancity Екатеринбург 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 он
принимает так же — ищет значение столбца среди его меток, как ключ в словаре:
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; у пяти из них нет либо плана, либо факта.
Сохраните в gap_m отклонение факта от плана по парам «город, месяц»; отсутствующий план или факт считайте
нулём. В kazan_march сохраните отклонение Казани в марте. В no_plan — сумму отклонений Екатеринбурга
за январь и февраль (месяцы, на которые плана не было).
fact_m.sub(plan_m, fill_value=0).gap_m.loc[("Казань", 3)]. Все месяцы города — gap_m.loc["Екатеринбург"]; его январь и февраль — срез меток .loc[1:2].Заготовка упражнения обновилась с тех пор, как вы сохранили свой код.
Вернуть заготовку? Ваш код в этом упражнении будет удалён.
gap_m = ...
kazan_march = ...
no_plan = ...Упражнение пока не решено, поэтому для ячеек ниже выполнено эталонное решение — ваш код не изменён.
gap_m = fact_m.sub(plan_m, fill_value=0)
kazan_march = gap_m.loc[("Казань", 3)]
no_plan = gap_m.loc["Екатеринбург"].loc[1:2].sum()Итоги
- Арифметика двух Series идёт по меткам индекса; порядок строк не важен.
- Метка без пары даёт
NaN; в результате остаются все метки обоих операндов. a.sub(b, fill_value=0)(иadd,mul,div) считает отсутствующее значение нулём.reindex(метки, fill_value=...)приводит Series к нужному набору меток.- Сравнение требует одинаковых меток в одинаковом порядке; запись Series в столбец тоже выравнивается по индексу.