Итоговый проект курса. У интернет-магазина «Кофейная лавка» — все продажи за 2025 год: файл
data/shop_orders.csv, 2448 строк. Владелец хочет понять, сколько магазин заработал и на чём. Ответим только
средствами NumPy — всем, что изучили за курс, и одним новым приёмом: суммами по группам. А в конце разберём
выгрузку службы доставки — с пропусками, как это бывает в жизни.
with open("data/shop_orders.csv") as f:
lines = f.read().splitlines()
print(len(lines) - 1, "строк")
for line in lines[:4]:
print(line)2448 строк order_id,date,customer_id,city,channel,category,product,price,quantity 10001,2025-01-01,C095,Москва,маркетплейс,Аксессуары,Кофемолка ручная,3200,2 10001,2025-01-01,C095,Москва,маркетплейс,Чай,Травяной сбор 50 г,260,1 10002,2025-01-01,C059,Москва,приложение,Сладости,Шоколад горький,180,1
Одна строка — один товар в заказе: номер заказа, дата, покупатель, город, канал продажи (сайт, приложение, маркетплейс), категория, товар, цена за штуку и количество. В одном заказе может быть несколько строк.
Шаг 1. Выручка и средний чек
Числовые столбцы загружает знакомый np.loadtxt с usecols= — можно передать несколько номеров сразу.
Текстовые столбцы он тоже умеет читать, если сказать dtype=str: получится массив строк.
Загрузите цену (столбец 7) и количество (столбец 8) и сохраните в revenue выручку каждой строки, а в total —
выручку за год. Затем загрузите номера заказов (столбец 0) и сохраните в n_orders число разных заказов,
а в avg_check — средний чек: выручка за год на один заказ.
np.loadtxt(путь, delimiter=",", skiprows=1, usecols=(7, 8)) — таблица из двух столбцов.np.unique(номера); их число — len или .size.Заготовка упражнения обновилась с тех пор, как вы сохранили свой код.
Вернуть заготовку? Ваш код в этом упражнении будет удалён.
import numpy as np
path = "data/shop_orders.csv"
revenue = ...
total = ...
n_orders = ...
avg_check = ...Упражнение пока не решено, поэтому для ячеек ниже выполнено эталонное решение — ваш код не изменён.
import numpy as np
path = "data/shop_orders.csv"
nums = np.loadtxt(path, delimiter=",", skiprows=1, usecols=(7, 8))
revenue = nums[:, 0] * nums[:, 1]
total = revenue.sum()
order_ids = np.loadtxt(path, delimiter=",", skiprows=1, usecols=0)
n_orders = len(np.unique(order_ids))
avg_check = total / n_ordersШаг 2. Суммы по группам
Сколько принесла каждая категория? Можно пройтись по категориям циклом и складывать выручку по маске. Но есть приём без цикла из двух шагов.
Коды групп. np.unique(a, return_inverse=True) возвращает не только уникальные значения, но и второй
массив — для каждого элемента a номер его значения среди уникальных. Так текст превращается в целые коды:
sample = np.array(["чай", "кофе", "чай", "какао", "кофе"])
names, codes = np.unique(sample, return_inverse=True)
print(names)
print(codes)
print(names[codes]) # коды → обратно в названия['какао' 'кофе' 'чай'] [2 1 2 0 1] ['чай' 'кофе' 'чай' 'какао' 'кофе']
Суммы по кодам. np.bincount из модуля о случайных числах считает, сколько раз встретился каждый код. С
параметром weights= он вместо единиц складывает веса: np.bincount(codes, weights=revenue) — сумма
выручки по каждому коду. Порядок сумм — порядок уникальных названий:
amounts = np.array([100, 250, 50, 80, 300]) # суммы тех же пяти покупок из sample
print(np.bincount(codes))
print(np.bincount(codes, weights=amounts))[1 2 2] [ 80. 550. 150.]
Загрузите столбец категорий (5) в category. Сохраните в cat_names названия категорий, в cat_revenue —
выручку каждой категории, а в cat_share — долю каждой категории в выручке года.
cat_names, cat_codes = np.unique(category, return_inverse=True).np.bincount(cat_codes, weights=revenue); доля — делим на сумму.Заготовка упражнения обновилась с тех пор, как вы сохранили свой код.
Вернуть заготовку? Ваш код в этом упражнении будет удалён.
category = ...
cat_names = ...
cat_revenue = ...
cat_share = ...Упражнение пока не решено, поэтому для ячеек ниже выполнено эталонное решение — ваш код не изменён.
category = np.loadtxt(path, delimiter=",", skiprows=1, usecols=5, dtype=str)
cat_names, cat_codes = np.unique(category, return_inverse=True)
cat_revenue = np.bincount(cat_codes, weights=revenue)
cat_share = cat_revenue / cat_revenue.sum()Кофе — больше половины выручки (58 %), чай — ещё 18 %.
Что выведет print(np.bincount(np.array([0, 2, 0]), weights=np.array([5.0, 1.0, 3.0])))?
weights получилось бы [2 0 1] — просто количества.Шаг 3. Лучшие и худшие товары
Загрузите столбец товаров (6) и сохраните в top5 названия пяти товаров с наибольшей выручкой — от первого
места к пятому, а в worst — название товара с наименьшей выручкой.
np.unique(…, return_inverse=True) и np.bincount(…, weights=revenue).np.argsort(суммы)[::-1][:5] — номера лидеров; названия — names[номера].Заготовка упражнения обновилась с тех пор, как вы сохранили свой код.
Вернуть заготовку? Ваш код в этом упражнении будет удалён.
top5 = ...
worst = ...Упражнение пока не решено, поэтому для ячеек ниже выполнено эталонное решение — ваш код не изменён.
product = np.loadtxt(path, delimiter=",", skiprows=1, usecols=6, dtype=str)
prod_names, prod_codes = np.unique(product, return_inverse=True)
prod_revenue = np.bincount(prod_codes, weights=revenue)
top5 = prod_names[np.argsort(prod_revenue)[::-1][:5]]
worst = prod_names[prod_revenue.argmin()]Шаг 4. Города
Загрузите столбец городов (3) и сохраните в city_names названия, в city_revenue — выручку каждого города,
а в moscow_share — долю Москвы в выручке года.
city_names можно найти маской: city_revenue[city_names == "Москва"] — массив из одного числа; возьмите [0].Заготовка упражнения обновилась с тех пор, как вы сохранили свой код.
Вернуть заготовку? Ваш код в этом упражнении будет удалён.
city_names = ...
city_revenue = ...
moscow_share = ...Упражнение пока не решено, поэтому для ячеек ниже выполнено эталонное решение — ваш код не изменён.
city = np.loadtxt(path, delimiter=",", skiprows=1, usecols=3, dtype=str)
city_names, city_codes = np.unique(city, return_inverse=True)
city_revenue = np.bincount(city_codes, weights=revenue)
moscow_share = city_revenue[city_names == "Москва"][0] / totalШаг 5. Грязная выгрузка: доставка
Данные о продажах были чистыми. Так бывает не всегда. Служба доставки прислала свою выгрузку — двумя файлами,
за первое и второе полугодие: data/delivery_h1.csv и data/delivery_h2.csv. В каждом по строке на заказ:
номер заказа, срок доставки в днях и оценка покупателя от 1 до 5.
with open("data/delivery_h1.csv") as f:
lines = f.read().splitlines()
print(len(lines) - 1, "заказов в первом полугодии")
for line in lines[:6]:
print(line)816 заказов в первом полугодии order_id,days,rating 10001,4, 10002,4,4 10003,2,5 10004,3,5 10005,4,3
В файлах есть пустые поля: оценку ставят не все покупатели, а срок курьеры иногда не записывают. Это работа для
приёмов из модуля о пропусках: np.genfromtxt, np.isnan, nan-функции, склейка массивов.
Загрузите оба файла и склейте их в одну таблицу delivery — сначала первое полугодие, потом второе. Сохраните
в gaps число пропусков в каждом столбце, а в avg_days — средний срок доставки по заказам, где он известен.
np.genfromtxt(путь, delimiter=",", skip_header=1); друг под другом — np.vstack([...]).np.isnan(delivery).sum(axis=0); средний срок — np.nanmean столбца 1.Заготовка упражнения обновилась с тех пор, как вы сохранили свой код.
Вернуть заготовку? Ваш код в этом упражнении будет удалён.
delivery = ...
gaps = ...
avg_days = ...Упражнение пока не решено, поэтому для ячеек ниже выполнено эталонное решение — ваш код не изменён.
h1 = np.genfromtxt("data/delivery_h1.csv", delimiter=",", skip_header=1)
h2 = np.genfromtxt("data/delivery_h2.csv", delimiter=",", skip_header=1)
delivery = np.vstack([h1, h2])
gaps = np.isnan(delivery).sum(axis=0)
avg_days = np.nanmean(delivery[:, 1])Оценок нет у трети заказов, срока — у 65. Выбрасывать такие строки целиком жалко: срок известен почти везде. Поэтому каждый вопрос считаем по тем строкам, где есть нужное для него значение.
Сохраните в late_share долю заказов, которые везли дольше 5 дней, — среди заказов с известным сроком.
В avg_rating — среднюю оценку среди поставленных. В fast_rating и slow_rating — среднюю оценку заказов,
доставленных за 3 дня и быстрее, и заказов, которые везли дольше 5 дней.
days[~np.isnan(days)]; доля — среднее маски > 5 по ним.rating[days <= 3], в них остались пропуски: среднюю считает np.nanmean.Заготовка упражнения обновилась с тех пор, как вы сохранили свой код.
Вернуть заготовку? Ваш код в этом упражнении будет удалён.
days = delivery[:, 1]
rating = delivery[:, 2]
late_share = ...
avg_rating = ...
fast_rating = ...
slow_rating = ...Упражнение пока не решено, поэтому для ячеек ниже выполнено эталонное решение — ваш код не изменён.
days = delivery[:, 1]
rating = delivery[:, 2]
late_share = (days[~np.isnan(days)] > 5).mean()
avg_rating = np.nanmean(rating)
fast_rating = np.nanmean(rating[days <= 3])
slow_rating = np.nanmean(rating[days > 5])print(f"выручка за год: {total:,.0f} ₽".replace(",", " "))
print(f"заказов: {n_orders}, средний чек: {avg_check:.0f} ₽")
for name, share in zip(cat_names, np.round(cat_share * 100, 1)):
print(f" {name}: {share} %")
print(f"доставка: в среднем {avg_days:.1f} дня, дольше 5 дней — {late_share * 100:.0f} % заказов")
print(f"оценка быстрых доставок {fast_rating:.1f}, медленных {slow_rating:.1f}")выручка за год: 3 301 420 ₽ заказов: 1576, средний чек: 2095 ₽ Аксессуары: 5.3 % Кофе: 58.0 % Посуда: 11.7 % Сладости: 6.7 % Чай: 18.4 % доставка: в среднем 4.3 дня, дольше 5 дней — 21 % заказов оценка быстрых доставок 4.2, медленных 2.7
Выводы
За 2025 год «Кофейная лавка» заработала 3.3 млн ₽ с 1576 заказов, средний чек — около 2100 ₽. Основа бизнеса —
кофе: 58 % выручки и вся пятёрка лидеров. Почти 40 % денег приносит Москва, ещё четверть — Петербург. Доставка
в среднем занимает 4.3 дня, но каждый пятый заказ везут дольше 5 дней — и оценивают такие заказы на 2.7 вместо
4.2 у быстрых: скорость доставки — первое, что стоит улучшать. Всё это —
без единого цикла по строкам: коды групп np.unique(…, return_inverse=True) и суммы np.bincount(…, weights=…).
В курсе pandas такие вопросы решает одна строка groupby — и вы будете понимать, что происходит внутри.