pandas: группировки, соединения и упорядоченные показатели

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' это гарантирует и сразу сообщает о проблеме, если в справочнике завелись дубликаты.