merge и concat: соединение таблиц с контролем кардинальности
Содержание курса
suffixes, indicator=True и validate: контроль качества соединения
После того как разобрались с тем, какие строки попадают в результат и откуда берутся NaN, следующий вопрос — как убедиться, что merge сделал именно то, что ожидалось. Для этого в pd.merge есть три параметра: suffixes, indicator и validate.
Конфликт имён столбцов и suffixes
Если оба DataFrame содержат столбец с одинаковым именем, и этот столбец не является ключом соединения, pandas не выбросит ошибку — он переименует оба, добавив суффиксы _x и _y:
import pandas as pd
orders = pd.DataFrame({
'order_id': [1, 2, 3],
'amount': [100, 200, 300],
'status': ['new', 'paid', 'new']
})
reviews = pd.DataFrame({
'order_id': [1, 2],
'status': ['ok', 'ok'],
'score': [4.5, 3.8]
})
result = pd.merge(orders, reviews, on='order_id', how='left')
print(result.columns.tolist())
# ['order_id', 'amount', 'status_x', 'status_y', 'score']
Столбец status оказался в обеих таблицах, и pandas создал status_x (из orders) и status_y (из reviews). Это работает, но читать такой код через неделю неудобно. Параметр suffixes позволяет задать понятные имена явно:
result = pd.merge(
orders, reviews,
on='order_id',
how='left',
suffixes=('_order', '_review')
)
print(result.columns.tolist())
# ['order_id', 'amount', 'status_order', 'status_review', 'score']
Теперь сразу понятно, откуда пришёл каждый status.
indicator=True: аудит происхождения строк
Параметр indicator=True добавляет в результат столбец _merge, который показывает, из какого источника пришла каждая строка. Возможные значения: 'both' — строка нашла пару в обоих DataFrame, 'left_only' — строка есть только в левом, 'right_only' — только в правом.
orders = pd.DataFrame({'order_id': [1, 2, 3], 'amount': [100, 200, 300]})
clients = pd.DataFrame({'order_id': [1, 2, 4], 'client': ['Аня', 'Боря', 'Вера']})
outer = pd.merge(orders, clients, on='order_id', how='outer', indicator=True)
print(outer)
# order_id amount client _merge
# 0 1 100.0 Аня both
# 1 2 200.0 Боря both
# 2 3 300.0 NaN left_only
# 3 4 NaN Вера right_only
Столбец _merge имеет категориальный dtype. Чаще всего его используют сразу после merge, чтобы посмотреть, сколько строк осталось без пары:
print(outer['_merge'].value_counts())
# both 2
# left_only 1
# right_only 1
Это удобнее, чем считать NaN вручную: indicator=True явно разделяет случай «строка без пары слева» и «строка без пары справа», а не просто фиксирует факт пропуска.
validate: проверка кардинальности до использования результата
Параметр validate позволяет задекларировать ожидаемую структуру соединения. Если реальные данные её нарушают, pandas выбрасывает MergeError — сразу, до того как результат будет где-то использован.
pandas принимает четыре значения: 'one_to_one', 'one_to_many', 'many_to_one' и 'many_to_many'. В этом уроке используем три проверяющих контракта, которые реально ограничивают уникальность ключей:
'one_to_one'— ключ уникален в обоих DataFrame'one_to_many'— ключ уникален в левом, может повторяться в правом'many_to_one'— ключ может повторяться в левом, уникален в правом
'many_to_many' тоже допустимо, но не проверяет уникальность ни в одной из сторон — оно лишь документирует намерение, не защищая от неожиданного декартова произведения.
Посмотрим, как работает проверка, когда ожидание не совпадает с реальностью:
orders_dup = pd.DataFrame({
'order_id': [1, 1, 2], # order_id=1 дублируется
'amount': [100, 150, 200]
})
clients = pd.DataFrame({'order_id': [1, 2, 4], 'client': ['Аня', 'Боря', 'Вера']})
try:
result = pd.merge(
orders_dup, clients,
on='order_id',
how='inner',
validate='one_to_one'
)
except pd.errors.MergeError as e:
print(e)
# Merge keys are not unique in left dataset; not a one-to-one merge
Если бы не было validate, merge молча выполнился бы и вернул три строки вместо двух — order_id=1 объединился бы с Аней дважды. Это декартово произведение по ключу, которое легко принять за корректный результат.
Когда дубликаты по ключу ожидаемы (например, один клиент — много заказов), используют 'many_to_one':
result = pd.merge(
orders_dup, clients,
on='order_id',
how='inner',
validate='many_to_one'
)
print(result.shape) # (3, 3) — корректно, validate не возражает
В практике validate особенно важен при соединении со справочниками: если справочник должен быть уникален по ключу, validate='many_to_one' это гарантирует и сразу сообщает о проблеме, если в справочнике завелись дубликаты.
