Вторая часть итогового проекта. Таблица собрана — теперь нужно ответить на вопросы владельца: как шёл год,
на чём магазин зарабатывает, где и кому продаёт. В отчёте везде используется чистая выручка net —
за вычетом возвратов.
Первая ячейка заново собирает таблицу из прошлого урока — то же самое, записанное тремя цепочками, плюс один
новый столбец: month, номер месяца. Прочитайте её: каждая строка вам знакома.
import pandas as pd
short = {"Спб": "Санкт-Петербург", "С.-Петербург": "Санкт-Петербург", "Екб": "Екатеринбург"}
customers = (
pd.read_csv("data/customers_raw.csv")
.assign(
customer_id=lambda d: d["customer_id"].str.strip().str.upper(),
city=lambda d: d["city"].str.strip().str.title().replace(short),
segment=lambda d: d["segment"].str.strip().str.lower(),
)
[["customer_id", "city", "segment"]]
.drop_duplicates()
)
returns = (
pd.read_csv("data/returns_raw.csv")
.drop_duplicates()
.drop_duplicates(subset=["order_id", "product_id"])
[["order_id", "product_id", "quantity"]]
.rename(columns={"quantity": "returned"})
)
data = (
pd.read_csv("data/orders.csv", parse_dates=["date"])
.merge(pd.read_csv("data/products.csv"), on="product_id", validate="many_to_one")
.merge(customers, on="customer_id", validate="many_to_one")
.merge(returns, on=["order_id", "product_id"], how="left", validate="one_to_one")
.assign(
returned=lambda d: d["returned"].fillna(0).astype("int64"),
revenue=lambda d: d["price"] * d["quantity"],
net=lambda d: d["price"] * (d["quantity"] - d["returned"]),
profit=lambda d: (d["price"] - d["cost"]) * (d["quantity"] - d["returned"]),
month=lambda d: d["date"].dt.month,
)
)
print(data.shape, data["net"].sum(), data["profit"].sum())(2448, 17) 3159140 1476130
Вопрос 1. Как шёл год
Сохраните в by_month таблицу по месяцам — сгруппируйте data по столбцу month: net и profit — суммы, growth — рост net к прошлому месяцу
(три знака). В best_month и worst_month сохраните номера лучшего и худшего месяца по чистой выручке.
data.groupby("month").agg(net=("net", "sum"), profit=("profit", "sum")).by_month["net"].pct_change().round(3); лучший и худший — idxmax() и idxmin().Заготовка упражнения обновилась с тех пор, как вы сохранили свой код.
Вернуть заготовку? Ваш код в этом упражнении будет удалён.
by_month = ...
# добавьте в by_month столбец growth
best_month = ...
worst_month = ...Упражнение пока не решено, поэтому для ячеек ниже выполнено эталонное решение — ваш код не изменён.
by_month = data.groupby("month").agg(net=("net", "sum"), profit=("profit", "sum"))
by_month["growth"] = by_month["net"].pct_change().round(3)
best_month = by_month["net"].idxmax()
worst_month = by_month["net"].idxmin()by_month| net | profit | growth | |
|---|---|---|---|
| month | |||
| 1 | 297120 | 140940 | NaN |
| 2 | 244070 | 116750 | -0.179 |
| 3 | 288570 | 135260 | 0.182 |
| 4 | 322960 | 147230 | 0.119 |
| 5 | 248930 | 115490 | -0.229 |
| 6 | 250030 | 114500 | 0.004 |
| 7 | 171250 | 79520 | -0.315 |
| 8 | 206820 | 96430 | 0.208 |
| 9 | 242450 | 110940 | 0.172 |
| 10 | 247020 | 113210 | 0.019 |
| 11 | 278680 | 129850 | 0.128 |
| 12 | 361240 | 176010 | 0.296 |
Вопрос 2. На чём зарабатываем
Сохраните в by_category таблицу по категориям: net, profit — суммы; sold — продано штук (quantity);
returned — возвращено штук; margin — прибыль, делённая на чистую выручку; return_rate — доля возвращённых
штук. Доли — три знака. Отсортируйте по убыванию прибыли.
agg; два вычисляемых столбца — после него.(by_category["profit"] / by_category["net"]).round(3) и (by_category["returned"] / by_category["sold"]).round(3).Заготовка упражнения обновилась с тех пор, как вы сохранили свой код.
Вернуть заготовку? Ваш код в этом упражнении будет удалён.
by_category = ...Упражнение пока не решено, поэтому для ячеек ниже выполнено эталонное решение — ваш код не изменён.
by_category = data.groupby("category").agg(net=("net", "sum"), profit=("profit", "sum"), sold=("quantity", "sum"), returned=("returned", "sum"))
by_category["margin"] = (by_category["profit"] / by_category["net"]).round(3)
by_category["return_rate"] = (by_category["returned"] / by_category["sold"]).round(3)
by_category = by_category.sort_values("profit", ascending=False)by_category| net | profit | sold | returned | margin | return_rate | |
|---|---|---|---|---|---|---|
| category | ||||||
| Кофе | 1830330 | 781430 | 2138 | 89 | 0.427 | 0.042 |
| Чай | 581140 | 325660 | 1598 | 69 | 0.560 | 0.043 |
| Посуда | 374350 | 187680 | 280 | 10 | 0.501 | 0.036 |
| Сладости | 212440 | 106260 | 1025 | 38 | 0.500 | 0.037 |
| Аксессуары | 160880 | 75100 | 169 | 8 | 0.467 | 0.047 |
Вопрос 3. Где продаём
Сохраните в by_city таблицу по городам: net — чистая выручка, orders — число разных заказов, buyers —
число разных покупателей, check — средний чек (net на заказ, до рублей), share — доля города в общей
чистой выручке (три знака). В top_check_city сохраните город с наибольшим средним чеком.
orders=("order_id", "nunique"), buyers=("customer_id", "nunique").(by_city["net"] / by_city["orders"]).round() и (by_city["net"] / by_city["net"].sum()).round(3).Заготовка упражнения обновилась с тех пор, как вы сохранили свой код.
Вернуть заготовку? Ваш код в этом упражнении будет удалён.
by_city = ...
top_check_city = ...Упражнение пока не решено, поэтому для ячеек ниже выполнено эталонное решение — ваш код не изменён.
by_city = data.groupby("city").agg(net=("net", "sum"), orders=("order_id", "nunique"), buyers=("customer_id", "nunique"))
by_city["check"] = (by_city["net"] / by_city["orders"]).round()
by_city["share"] = (by_city["net"] / by_city["net"].sum()).round(3)
top_check_city = by_city["check"].idxmax()Вопрос 4. Что и где
Сохраните в cross сводную таблицу чистой выручки: категории в строках, города в столбцах, с итогами «Всего».
В moscow_coffee_share сохраните долю московского кофе во всей чистой выручке магазина — посчитайте её
по ячейкам cross.
data.pivot_table(values="net", index="category", columns="city", aggfunc="sum", margins=True, margins_name="Всего").cross.loc["Кофе", "Москва"] / cross.loc["Всего", "Всего"].Заготовка упражнения обновилась с тех пор, как вы сохранили свой код.
Вернуть заготовку? Ваш код в этом упражнении будет удалён.
cross = ...
moscow_coffee_share = ...Упражнение пока не решено, поэтому для ячеек ниже выполнено эталонное решение — ваш код не изменён.
cross = data.pivot_table(values="net", index="category", columns="city", aggfunc="sum", margins=True, margins_name="Всего")
moscow_coffee_share = cross.loc["Кофе", "Москва"] / cross.loc["Всего", "Всего"]cross| city | Екатеринбург | Казань | Москва | Новосибирск | Санкт-Петербург | Всего |
|---|---|---|---|---|---|---|
| category | ||||||
| Аксессуары | 28370 | 30940 | 51300 | 11050 | 39220 | 160880 |
| Кофе | 222050 | 247600 | 735480 | 172860 | 452340 | 1830330 |
| Посуда | 54500 | 48220 | 154550 | 39540 | 77540 | 374350 |
| Сладости | 29480 | 25350 | 82350 | 18040 | 57220 | 212440 |
| Чай | 75100 | 73390 | 221380 | 62060 | 149210 | 581140 |
| Всего | 409500 | 425500 | 1245060 | 303550 | 775530 | 3159140 |
Вопрос 5. Кому продаём
Сохраните в by_segment таблицу по сегментам: net, buyers (число разных покупателей) и per_buyer —
чистая выручка на покупателя, до рублей. В vip сохраните пять покупателей с наибольшей чистой выручкой:
столбцы customer_id, city, segment, net, индекс с нуля.
vip — группировка по трём столбцам с as_index=False, сумма net, .nlargest(5, "net").Заготовка упражнения обновилась с тех пор, как вы сохранили свой код.
Вернуть заготовку? Ваш код в этом упражнении будет удалён.
by_segment = ...
vip = ...Упражнение пока не решено, поэтому для ячеек ниже выполнено эталонное решение — ваш код не изменён.
by_segment = data.groupby("segment").agg(net=("net", "sum"), buyers=("customer_id", "nunique"))
by_segment["per_buyer"] = (by_segment["net"] / by_segment["buyers"]).round()
vip = data.groupby(["customer_id", "city", "segment"], as_index=False)["net"].sum().nlargest(5, "net").reset_index(drop=True)Графики
Постройте линейный график столбцов net и profit таблицы by_month с заголовком «Чистая выручка и прибыль
по месяцам, 2025» и сохраните в ax. Подпишите оси: X — «месяц», Y — «₽».
by_month[["net", "profit"]].plot(title="...").Заготовка упражнения обновилась с тех пор, как вы сохранили свой код.
Вернуть заготовку? Ваш код в этом упражнении будет удалён.
ax = ...
# подпишите осиУпражнение пока не решено, поэтому для ячеек ниже выполнено эталонное решение — ваш код не изменён.
ax = by_month[["net", "profit"]].plot(title="Чистая выручка и прибыль по месяцам, 2025")
ax.set_xlabel("месяц")
ax.set_ylabel("₽")by_category["profit"].sort_values().plot(kind="barh", title="Прибыль по категориям, ₽")Отчёт
total_net = data["net"].sum()
total_profit = data["profit"].sum()
print("Выручка:", data["revenue"].sum(), "₽ · после возвратов:", total_net, "₽")
print("Прибыль:", total_profit, "₽ · маржа:", round(total_profit / total_net * 100, 1), "%")
print("Заказов:", data["order_id"].nunique(), "· покупателей:", data["customer_id"].nunique())
print("Лучший месяц:", best_month, "· худший:", worst_month)
print("Самый высокий средний чек:", top_check_city)
print(by_segment)Выручка: 3301420 ₽ · после возвратов: 3159140 ₽
Прибыль: 1476130 ₽ · маржа: 46.7 %
Заказов: 1576 · покупателей: 213
Лучший месяц: 12 · худший: 7
Самый высокий средний чек: Санкт-Петербург
net buyers per_buyer
segment
новый 1109020 86 12896.0
оптовый 737000 25 29480.0
постоянный 1313120 102 12874.0Выводы
Год в целом. Магазин продал товаров на 3.30 млн ₽; после возвратов осталось 3.16 млн ₽, из них прибыль — 1.48 млн ₽, маржа 46.7 %. 1576 заказов от 213 покупателей.
Сезонность. Лучший месяц — декабрь (361 тыс. ₽, +30 % к ноябрю), худший — июль (171 тыс. ₽, −32 % к июню). Лето — провал, четвёртый квартал — пик. К летнему спаду стоит готовиться заранее: акциями или холодными напитками в ассортименте.
Категории. Больше половины прибыли даёт кофе, но маржа у него самая низкая — 43 %. Самая маржинальная категория — чай (56 %), и он же хорошо продаётся зимой. Возвращают 4 % проданных штук, и доля возвратов по категориям почти одинакова — проблемной категории нет.
Города. Москва — 39 % чистой выручки, Санкт-Петербург — четверть. Средний чек по городам отличается мало: от 1860 ₽ в Новосибирске до 2100 ₽ в Санкт-Петербурге.
Покупатели. 25 оптовых покупателей приносят почти четверть выручки: на каждого приходится 29 тыс. ₽ в год — в два с лишним раза больше, чем на обычного. Четверо из пяти лучших покупателей — оптовые. Потеря даже нескольких таких клиентов заметно ударит по выручке.
Что вы теперь умеете
За курс вы прошли весь путь аналитика: загрузить данные, осмотреть, почистить, соединить, посчитать, свернуть, показать и объяснить. Этот отчёт — не учебное упражнение, а обычная рабочая задача, и вы решили её сами.
Дальше — практика на своих данных. Возьмите любую выгрузку, которая вам интересна, и задайте ей вопросы. Когда забудется название метода или параметра — справочник под рукой.