Мира · маркетолог магазина настолок «Кубик»
Финансисты прислали продажи за квартал тремя файлами, по месяцу в каждом. У меня план по жанрам в Excel: жанры строками, месяцы столбцами. Сведи план с фактом и скажи, какие жанры не дотянули. Только без выручки вдвое, как тогда с доставками.
В уроке про pandas и SQL мы соединили покупки с офферами и собрали сводную таблицу. В прошлом уроке почистили выгрузку и сверили её с SQL. Сегодня таблиц много: три месяца продаж, каталог, доставки и план. Учимся склеивать их так, чтобы pandas сам кричал, когда строки размножаются, и переворачивать таблицы из широкой формы в длинную и обратно.
Три файла в один: concat
Финансисты выгрузили позиции заказов базы boardshop: в апрельском файле 834 строки, в майском 850, в июньском 760. Столбцы у всех одинаковые, значит файлы можно поставить друг под друга, как UNION ALL из урока про операции над множествами:
import pandas as pd
months = ["2026-04", "2026-05", "2026-06"]
parts = [pd.read_csv(f"sales_{m}.csv") for m in months]
sales = pd.concat(parts, ignore_index=True)
len(sales) # 2444
834 + 850 + 760 = 2444, ни одна строка не потерялась. ignore_index=True перенумеровывает строки заново. Без него в склейке три строки с номером 0, три с номером 1, и sales.loc[0] вернёт не строку, а маленькую таблицу из трёх. Мелочь, пока на номер никто не опирается.
Отменённые заказы в выручку не идут, а месяц достанем из даты:
sales = sales[sales["status"] != "cancelled"]
sales["revenue"] = sales["qty"] * sales["price"]
sales["month"] = sales["created_at"].str[:7]
sales.groupby("month")["revenue"].sum()
month
2026-04 1552052.0
2026-05 1655296.0
2026-06 1564127.0
Те же числа даёт SQL с date_trunc по месяцам. 146 позиций отменённых заказов на 285 132 ₽ остались за бортом.
Каталог: ключ проверяет validate
Жанра в продажах нет, он в каталоге products. Позиций много, а игра у каждой одна: это соединение «многие к одному». Его и пишем прямо в вызове:
catalog = products[["id", "genre"]].rename(columns={"id": "product_id"})
lines = sales.merge(catalog, on="product_id", how="left", validate="many_to_one")
Параметр validate это обещание о ключах. "many_to_one" значит: слева ключ может повторяться, справа нет. Пока каталог чистый, ничего не происходит. А теперь представь, что в каталоге «Хроники Ледника» задвоились из-за правки карточки:
MergeError: Merge keys are not unique in right dataset; not a many-to-one merge
Без validate никакой ошибки не будет. Каждая из 78 проданных «Хроник» встретит обе карточки, строк станет на 78 больше, а выручка вырастет на 211 914 ₽. Это тот самый ксерокс из урока про размножение строк, только в pandas: merge честно делает строку на каждую пару. validate ловит его до того, как он доберётся до суммы.
Доставки: fan-out остановлен на входе
Теперь проверим соединение, которое опасно по-настоящему. Мира хочет видеть, какая служба везёт июньские заказы. У заказа может быть две попытки доставки: курьер не застал клиента и приехал снова. Пишем так, как думаем, «у заказа одна доставка»:
orders.merge(deliveries, left_on="id", right_on="order_id", validate="one_to_one")
MergeError: Merge keys are not unique in right dataset; not a one-to-one merge
pandas отказался, и правильно. Что было бы без проверки? Наивное соединение позиций с доставками насчитывает за июнь 1 586 400 ₽ вместо 1 564 127 ₽. Две ошибки разного знака: 28 заказов с двумя попытками посчитались дважды, плюс 96 556 ₽, а 12 новых заказов без доставки merge по умолчанию выбросил, минус 74 283 ₽. Итог отличается всего на 1,4%, и на глаз такое не ловится. Это ровно две ошибки отчёта Миры из урока 26.
Лечение то же, что в SQL: оставить из «многих» одну строку. Возьмём последнюю попытку, она говорит, где заказ сейчас:
last = (deliveries.sort_values("attempt")
.drop_duplicates(subset="order_id", keep="last"))
len(deliveries), len(last) # (413, 385)
Теперь у каждого заказа не больше одной доставки, и validate="one_to_one" пропускает соединение.
урок из курса
Урок из курса «Аналитика данных с нуля». Курс покупается один раз и остаётся навсегда: 500 ₽.
Оплата пока не подключена: премиум и курсы открываются промокодом.