В начале года «Кофейная лавка» составила план продаж: сколько выручки должен дать каждый город в каждом
месяце. Год закончился — пора сравнить план с фактом. План лежит в data/plan.csv, факт нужно собрать
из таблицы продаж. Новых методов в проекте нет.
import pandas as pd
orders = pd.read_csv("data/shop_orders.csv")
orders["revenue"] = orders["price"] * orders["quantity"]
orders["month"] = pd.to_datetime(orders["date"]).dt.month
plan_table = pd.read_csv("data/plan.csv")
plan_table.head(4)| city | month | plan | |
|---|---|---|---|
| 0 | Москва | 1 | 132000 |
| 1 | Москва | 2 | 126000 |
| 2 | Москва | 3 | 108000 |
| 3 | Москва | 4 | 110000 |
Шаг 1. Два Series с общим индексом
Чтобы pandas сам сопоставил план и факт, у обоих должен быть одинаково устроенный индекс: город и месяц.
Сохраните в fact_m выручку по парам «город, месяц» — Series с индексом из двух уровней. Сохраните в plan_m
план в таком же виде: Series со значениями столбца plan и индексом из city и month.
orders.groupby(["city", "month"])["revenue"].sum().plan_table.set_index(["city", "month"])["plan"] — после set_index берём один столбец.Заготовка упражнения обновилась с тех пор, как вы сохранили свой код.
Вернуть заготовку? Ваш код в этом упражнении будет удалён.
fact_m = ...
plan_m = ...Упражнение пока не решено, поэтому для ячеек ниже выполнено эталонное решение — ваш код не изменён.
fact_m = orders.groupby(["city", "month"])["revenue"].sum()
plan_m = plan_table.set_index(["city", "month"])["plan"]Шаг 2. План и факт рядом
Таблицу можно собрать из словаря Series — как в самом начале курса из словаря списков. Series при этом выравниваются по меткам: строка результата — пара «город, месяц», а значения подтягиваются из обоих Series.
left = pd.Series([1, 2], index=["x", "y"])
right = pd.Series([30, 40], index=["y", "z"])
pd.DataFrame({"left": left, "right": right})| left | right | |
|---|---|---|
| x | 1.0 | NaN |
| y | 2.0 | 30.0 |
| z | NaN | 40.0 |
Метка y есть в обоих Series — в её строке заполнены оба столбца. У x и z пары нет — там NaN.
Соберите таблицу pf со столбцами plan и fact из plan_m и fact_m. Сохраните в no_plan число пар без
плана, в no_fact — число пар без факта. Затем замените пропуски в столбце fact нулями.
pd.DataFrame({"plan": plan_m, "fact": fact_m}).pf["plan"].isna().sum(), — потом заполняйте: pf["fact"] = pf["fact"].fillna(0).Заготовка упражнения обновилась с тех пор, как вы сохранили свой код.
Вернуть заготовку? Ваш код в этом упражнении будет удалён.
pf = ...
no_plan = ...
no_fact = ...
# замените пропуски в столбце fact нулямиУпражнение пока не решено, поэтому для ячеек ниже выполнено эталонное решение — ваш код не изменён.
pf = pd.DataFrame({"plan": plan_m, "fact": fact_m})
no_plan = pf["plan"].isna().sum()
no_fact = pf["fact"].isna().sum()
pf["fact"] = pf["fact"].fillna(0)Почему факт заполняем нулём, а план — нет? Пропуск в факте значит «продаж не было»: это ноль рублей. Пропуск в плане значит «план не ставили» — и это не «план ноль рублей». Если записать туда ноль, доля выполнения станет делением на ноль, а отклонение — «перевыполнением» несуществующего плана.
Шаг 3. Отклонение и выполнение
Добавьте в pf столбец gap — факт минус план — и столбец done — факт, делённый на план, с округлением
до трёх знаков. Затем сохраните в flat плоскую версию таблицы: city и month — обычные столбцы.
pf["fact"] - pf["plan"] и (pf["fact"] / pf["plan"]).round(3).pf.reset_index().Заготовка упражнения обновилась с тех пор, как вы сохранили свой код.
Вернуть заготовку? Ваш код в этом упражнении будет удалён.
# добавьте в pf столбцы gap и done
flat = ...Упражнение пока не решено, поэтому для ячеек ниже выполнено эталонное решение — ваш код не изменён.
pf["gap"] = pf["fact"] - pf["plan"]
pf["done"] = (pf["fact"] / pf["plan"]).round(3)
flat = pf.reset_index()flat[flat["city"] == "Екатеринбург"].head(4)| city | month | plan | fact | gap | done | |
|---|---|---|---|---|---|---|
| 0 | Екатеринбург | 1 | NaN | 21070.0 | NaN | NaN |
| 1 | Екатеринбург | 2 | NaN | 32470.0 | NaN | NaN |
| 2 | Екатеринбург | 3 | 46000.0 | 44490.0 | -1510.0 | 0.967 |
| 3 | Екатеринбург | 4 | 52000.0 | 49790.0 | -2210.0 | 0.958 |
Там, где плана нет, gap и done остались пропусками — арифметика с NaN даёт NaN, и это правильно:
оценить выполнение несуществующего плана нельзя.
Шаг 4. По месяцам
По таблице flat соберите by_month — таблицу по месяцам: plan и fact — суммы по всем городам, done —
их отношение (три знака). В months_ok сохраните число месяцев, когда факт не меньше плана, в worst_month —
номер месяца с наименьшим done. Выполнен ли план, решайте по самим суммам, а не по округлённой доле done:
после округления до трёх знаков почти выполненный план может превратиться в 1.0.
flat.groupby("month").agg(plan=("plan", "sum"), fact=("fact", "sum")).(by_month["fact"] >= by_month["plan"]).sum() и by_month["done"].idxmin().Заготовка упражнения обновилась с тех пор, как вы сохранили свой код.
Вернуть заготовку? Ваш код в этом упражнении будет удалён.
by_month = ...
months_ok = ...
worst_month = ...Упражнение пока не решено, поэтому для ячеек ниже выполнено эталонное решение — ваш код не изменён.
by_month = flat.groupby("month").agg(plan=("plan", "sum"), fact=("fact", "sum"))
by_month["done"] = (by_month["fact"] / by_month["plan"]).round(3)
months_ok = (by_month["fact"] >= by_month["plan"]).sum()
worst_month = by_month["done"].idxmin()Шаг 5. По городам
Добавьте в flat столбец ok: True, если факт не меньше плана. Сравнивайте сами суммы, а не долю done:
она округлена, и 0.9997 превратилось в 1.0. Соберите by_city — таблицу по городам:
plan, fact — суммы, months_ok — число месяцев с выполненным планом. Добавьте столбец gap — факт минус
план. В steadiest сохраните город с наибольшим числом выполненных месяцев.
True: months_ok=("ok", "sum").by_city["months_ok"].idxmax().Заготовка упражнения обновилась с тех пор, как вы сохранили свой код.
Вернуть заготовку? Ваш код в этом упражнении будет удалён.
# добавьте в flat столбец ok
by_city = ...
# добавьте в by_city столбец gap
steadiest = ...Упражнение пока не решено, поэтому для ячеек ниже выполнено эталонное решение — ваш код не изменён.
flat["ok"] = flat["fact"] >= flat["plan"]
by_city = flat.groupby("city").agg(plan=("plan", "sum"), fact=("fact", "sum"), months_ok=("ok", "sum"))
by_city["gap"] = by_city["fact"] - by_city["plan"]
steadiest = by_city["months_ok"].idxmax()by_city.sort_values("gap")| plan | fact | months_ok | gap | |
|---|---|---|---|---|
| city | ||||
| Сочи | 60000.0 | 0.0 | 0 | -60000.0 |
| Санкт-Петербург | 841000.0 | 803720.0 | 5 | -37280.0 |
| Казань | 451000.0 | 440150.0 | 4 | -10850.0 |
| Новосибирск | 329000.0 | 324090.0 | 4 | -4910.0 |
| Москва | 1313000.0 | 1310110.0 | 7 | -2890.0 |
| Екатеринбург | 398000.0 | 423350.0 | 2 | 25350.0 |
Шаг 6. Карта выполнения
Сохраните в heat сводную таблицу выполнения плана done: города в строках, месяцы в столбцах, округление
до двух знаков. В moscow_worst сохраните номер месяца, в котором Москва выполнила план хуже всего.
flat.pivot_table(values="done", index="city", columns="month", aggfunc="mean").round(2).heat.loc["Москва"].idxmin().Заготовка упражнения обновилась с тех пор, как вы сохранили свой код.
Вернуть заготовку? Ваш код в этом упражнении будет удалён.
heat = ...
moscow_worst = ...Упражнение пока не решено, поэтому для ячеек ниже выполнено эталонное решение — ваш код не изменён.
heat = flat.pivot_table(values="done", index="city", columns="month", aggfunc="mean").round(2)
moscow_worst = heat.loc["Москва"].idxmin()heat| month | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| city | ||||||||||||
| Екатеринбург | NaN | NaN | 0.97 | 0.96 | 0.86 | 1.06 | 1.10 | 0.84 | 0.84 | 0.98 | 0.89 | 0.90 |
| Казань | 1.16 | 0.84 | 0.94 | 0.86 | 1.00 | 1.08 | 0.89 | 0.94 | 0.89 | 1.09 | 1.12 | 0.89 |
| Москва | 1.07 | 0.85 | 1.10 | 1.10 | 1.02 | 1.07 | 0.92 | 1.12 | 0.86 | 0.87 | 1.17 | 0.96 |
| Новосибирск | 1.17 | 0.85 | 1.11 | 0.94 | 1.00 | 1.05 | 1.11 | 0.92 | 0.86 | 0.89 | 0.93 | 0.91 |
| Санкт-Петербург | 1.12 | 1.06 | 1.00 | 0.99 | 0.98 | 0.85 | 1.08 | 1.09 | 0.92 | 0.84 | 0.86 | 0.86 |
| Сочи | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | 0.00 | 0.00 | 0.00 |
Выводы
- План года не выполнен в четырёх городах из пяти; хуже всех — Санкт-Петербург (−37 тыс. ₽). Екатеринбург формально в плюсе, но только потому, что на январь и февраль плана у него не было.
- Сочи — 60 тыс. ₽ плана и ноль продаж: магазин туда так и не вышел.
- По месяцам план выполнен пять раз из двенадцати, и все эти месяцы — в первых восьми. С сентября по декабрь общий факт ниже плана; хуже всего октябрь (85 %).
- Сентябрь и декабрь провалены во всех городах сразу — значит, дело не в городе, а в плане: он был слишком оптимистичен. Октябрь, ноябрь и декабрь выглядят хуже, чем есть: в план каждого входит 20 тыс. ₽ Сочи, где продаж не было. Без Сочи ноябрь выполнен, а худшим месяцем становится сентябрь (88 %).
Главная техника проекта — выравнивание: мы ни разу не сопоставляли план и факт вручную. Достаточно было дать обоим Series одинаковый индекс.