Нашли ошибку?
Скачать чистый
урок 3 из 4≈ 20 минупражнения: 0 из 3

Ключи и ловушки

Ключи с разными названиями — left_on и right_on; одноимённые столбцы — suffixes; соединение по двум ключам; главная ловушка merge — повторы ключа, которые размножают строки; проверки validate и indicator.

Python запустится при первом выполнении

В прошлом уроке всё было удобно: ключ назывался одинаково, столбцы не совпадали, у каждого кода товара была одна строка в справочнике. В жизни так бывает не всегда — и соединение, которое «просто сработало», может тихо испортить данные.

[ ]Три таблицы магазина
import pandas as pd

orders = pd.read_csv("data/orders.csv")
products = pd.read_csv("data/products.csv")
customers = pd.read_csv("data/customers.csv")
print(list(products.columns))
print(list(customers.columns))
Выводсохранённый · нажмите «Выполнить», чтобы получить свой
['product_id', 'name', 'category', 'price', 'cost']
['customer_id', 'name', 'city', 'signup_date', 'segment']

Ключ называется по-разному

Если ключевой столбец в таблицах назван по-разному, вместо on указывают два параметра: left_on — название в левой таблице, right_on — в правой.

[ ]product_id слева, id справа
catalog = products.rename(columns={"product_id": "id"})
joined = orders.merge(catalog, left_on="product_id", right_on="id")
list(joined.columns)
Выводсохранённый · нажмите «Выполнить», чтобы получить свой
['order_id', 'date', 'customer_id', 'product_id', 'quantity', 'channel', 'id', 'name', 'category', 'price', 'cost']

В результате остались оба ключевых столбца — product_id и id — с одинаковым содержимым. Лишний обычно удаляют: .drop(columns=["id"]).

Одноимённые столбцы

В справочниках товаров и клиентов есть столбец name: название товара и имя покупателя. Если соединить всё подряд, pandas различит их окончаниями _x и _y:

[ ]name_x и name_y
both = orders.merge(products, on="product_id").merge(customers, on="customer_id")
list(both.columns)
Выводсохранённый · нажмите «Выполнить», чтобы получить свой
['order_id', 'date', 'customer_id', 'product_id', 'quantity', 'channel', 'name_x', 'category', 'price', 'cost', 'name_y', 'city', 'signup_date', 'segment']

name_x пришёл из левой таблицы, name_y — из правой. Понять это через месяц будет невозможно. Свои окончания задаёт параметр suffixes — пара строк для левой и правой таблицы:

[ ]Понятные окончания
both = orders.merge(products, on="product_id").merge(customers, on="customer_id", suffixes=("_product", "_customer"))
both[["order_id", "name_product", "name_customer", "city"]].head(3)
Выводсохранённый · нажмите «Выполнить», чтобы получить свой
order_id name_product name_customer city
0 10001 Кофемолка ручная Ирина О. Москва
1 10001 Травяной сбор 50 г Ирина О. Москва
2 10002 Шоколад горький Светлана П. Москва

Окончания добавляются только к столбцам, названия которых совпали. Второй способ избежать путаницы — заранее взять из правой таблицы только нужные столбцы или переименовать их через rename.

[ ]Упражнение 1Новая себестоимость решено

Поставщик прислал прайс supplier с новой себестоимостью (cost) и остатками (stock). Соедините products с тремя столбцами прайса — product_id, cost, stock — и сохраните в compare; одноимённые столбцы должны называться cost_now и cost_new. Переведите cost_new в числа (в прайсе встречается «нет данных») и добавьте столбец cost_change — на сколько выросла себестоимость.

supplier = pd.read_csv("data/supplier_prices.csv")
compare = ...
# переведите cost_new в числа и добавьте столбец cost_change

Неверный ключ

Главная ловушка merge. Возьмём очищенный журнал возвратов — 150 строк, по одной на возвращённую позицию:

[ ]Возвраты: заказ, товар, сколько штук вернули
returns = pd.read_csv("data/returns_raw.csv").drop_duplicates()
returns = returns.drop_duplicates(subset=["order_id", "product_id"])[["order_id", "product_id", "quantity"]]
print(len(returns))
returns.head(3)
Выводсохранённый · нажмите «Выполнить», чтобы получить свой
150
order_id product_id quantity
0 10001 P18 2
1 10010 P16 1
2 10012 P01 2

Хотим добавить к строкам заказов число возвращённых штук. В обеих таблицах есть столбец quantity — зададим окончания так, чтобы левый остался как есть, а правый стал quantity_returned: пустая строка в suffixes значит «название не менять». Попробуем соединить по номеру заказа:

[ ]Соединение по order_id
wrong = orders.merge(returns[["order_id", "quantity"]], on="order_id", how="left", suffixes=("", "_returned"))
print(len(orders), len(wrong))
print(wrong["quantity_returned"].sum(), returns["quantity"].sum())
Выводсохранённый · нажмите «Выполнить», чтобы получить свой
2448 2458
402.0 214

Строк стало 2458 вместо 2448, а возвращённых штук — 402 вместо 214. Почти вдвое больше, чем на самом деле. Здесь сразу две ошибки, и у них одна причина: ключ order_id не определяет строку однозначно.

Первая: значение приклеилось не к той строке. Посмотрим на заказ 10001:

[ ]Заказ 10001 после соединения
wrong.loc[wrong["order_id"] == 10001, ["order_id", "product_id", "quantity", "quantity_returned"]]
Выводсохранённый · нажмите «Выполнить», чтобы получить свой
order_id product_id quantity quantity_returned
0 10001 P18 2 2.0
1 10001 P09 1 2.0

В заказе две позиции — кофемолка (P18) и травяной сбор (P09). Вернули только кофемолку, 2 штуки. Но соединение шло по номеру заказа, и возврат «приклеился» к обеим позициям: получилось, что вернули и сбор тоже. Число строк при этом не изменилось — такую ошибку по len не заметить. Её выдаёт только сверка сумм: 402 против 214.

Вторая: строки размножились. В заказе 10119 вернули оба товара — в журнале две записи с этим номером:

[ ]Заказ 10119: две позиции и два возврата дали четыре строки
print(returns[returns["order_id"] == 10119])
wrong.loc[wrong["order_id"] == 10119, ["order_id", "product_id", "quantity", "quantity_returned"]]
Выводсохранённый · нажмите «Выполнить», чтобы получить свой
    order_id product_id  quantity
12     10119        P07         1
15     10119        P05         1
order_id product_id quantity quantity_returned
190 10119 P07 3 1.0
191 10119 P07 3 1.0
192 10119 P05 1 1.0
193 10119 P05 1 1.0

merge соединяет каждую строку слева с каждой подходящей строкой справа. Две позиции заказа и две записи о возврате дали 2 × 2 = 4 строки. Если ключ повторяется с обеих сторон, строки перемножаются.

Вопрос
a = pd.DataFrame({"k": ["x", "x"], "a": [1, 2]})
b = pd.DataFrame({"k": ["x", "x"], "b": [3, 4]})
print(len(a.merge(b, on="k")))

Что выведет этот код?

Соединение по нескольким ключам

Возврат относится не к заказу, а к позиции — паре «заказ, товар». Значит, и ключ составной: в on передают список столбцов, и строки сопоставляются, только если совпали оба.

[ ]Упражнение 2Сколько вернули решено

Соедините orders с returns по двум ключам так, чтобы остались все строки заказов, а столбцы количества назывались quantity и quantity_returned. Сохраните в lines. Пропуски в quantity_returned замените нулями и сделайте столбец целым. В return_rate сохраните долю возвращённых штук среди проданных.

lines = ...
# замените пропуски в lines["quantity_returned"] нулями, тип — целый
return_rate = ...

[ ]Те же два заказа после соединения по двум ключам
print(len(lines), lines["quantity_returned"].sum())
lines.loc[lines["order_id"].isin([10001, 10119]), ["order_id", "product_id", "quantity", "quantity_returned"]]
Выводсохранённый · нажмите «Выполнить», чтобы получить свой
2448 214
order_id product_id quantity quantity_returned
0 10001 P18 2 2
1 10001 P09 1 0
190 10119 P07 3 1
191 10119 P05 1 1
Вывод появится, когда решите упражнение «Сколько вернули», — иначе он подскажет ответ. Ячейку можно и выполнить.

Теперь всё сходится: 2448 строк, 214 возвращённых штук; в заказе 10001 возврат стоит только у кофемолки, в заказе 10119 — по одной штуке у каждого товара.

Пояснение с ответом появится, когда решите упражнение «Сколько вернули».

validate: проверка при соединении

Проверять число строк вручную легко забыть. Параметр validate делает проверку частью соединения: вы сообщаете, какое соединение ожидаете, а pandas останавливается с ошибкой, если это не так.

  • "many_to_one" — ключ в правой таблице уникален (заказы → справочник);
  • "one_to_one" — ключ уникален в обеих;
  • "one_to_many" — уникален в левой.
[ ]Заказы к справочнику: многие к одному
checked = orders.merge(products, on="product_id", validate="many_to_one")
len(checked)
Выводсохранённый · нажмите «Выполнить», чтобы получить свой
2448

А соединение по одному order_id проверку не проходит — в правой таблице номер заказа повторяется:

[ ]Ключ в правой таблице повторяетсяошибка: MergeError
orders.merge(returns[["order_id", "quantity"]], on="order_id", how="left", validate="many_to_one")
Ошибкасохранённый · нажмите «Выполнить», чтобы получить свой
MergeError: Merge keys are not unique in right dataset; not a many-to-one merge

Duplicates in right:
  order_id
    10119
    10289
    10498
    10594 ...

Ошибка MergeError лучше, чем тихо завышенные числа: соединение не состоялось, а в сообщении перечислены ключи, которые повторяются в правой таблице. Но помните про первую ошибку из примера выше: validate ловит только повторы ключа. Значение, приклеившееся не к той строке, он не заметит — это видно лишь по суммам.

indicator: откуда строка

Параметр indicator=True добавляет столбец _merge, в котором записано, нашлась ли пара: both — ключ есть в обеих таблицах, left_only — только в левой, right_only — только в правой.

[ ]Клиенты, у которых есть и нет заказов
buyers = orders[["customer_id"]].drop_duplicates()
marked = customers.merge(buyers, on="customer_id", how="left", indicator=True)
marked["_merge"].value_counts()
Выводсохранённый · нажмите «Выполнить», чтобы получить свой
_merge
both          213
left_only      27
right_only      0
Name: count, dtype: int64

27 клиентов — left_only: в заказах их нет. Это те же «спящие» клиенты из прошлого урока, найденные без подсчёта заказов. indicator удобен для сверки двух списков: что есть в одном и отсутствует в другом.

[ ]Упражнение 3Что возвращают чаще всего решено

По таблице lines соберите sold — плоскую таблицу по товарам: product_id, sold (продано штук), returned (возвращено штук). Добавьте столбец rate — долю возврата, округлённую до трёх знаков. Затем присоедините названия товаров из products и сохраните в rates, отсортировав по убыванию rate. В worst_name сохраните название товара с наибольшей долей возврата.

sold = ...
# добавьте в sold столбец rate
rates = ...
worst_name = ...

Итоги

  • Ключи с разными названиями — left_on= и right_on=.
  • Совпавшие названия столбцов получают окончания _x и _y; свои задаёт suffixes=("_a", "_b").
  • Несколько ключей — on=["a", "b"].
  • Неверный ключ размножает строки и значения: сверяйте len и суммы до и после, пишите validate="many_to_one".
  • indicator=True показывает, у каких строк не нашлось пары.

Подробнее в справочнике

Нашли ошибку?