merge и concat: соединение таблиц с контролем кардинальности
Содержание курса
Происхождение NaN в результатах left, right и outer join
Когда merge не находит пару для строки, он не выбрасывает её и не ставит заглушку — он заполняет недостающие поля NaN. Это не баг и не случайность: это явный сигнал о том, что данных с другой стороны не было.
Разберём на тех же таблицах:
import pandas as pd
orders = pd.DataFrame({'order_id': [1, 2, 3], 'amount': [100, 200, 300]})
clients = pd.DataFrame({'order_id': [1, 2, 4], 'client': ['Аня', 'Боря', 'Вера']})
При left join левый DataFrame — orders — сохраняется полностью. order_id=3 есть в orders, но в clients такой записи нет. pandas добавляет строку с order_id=3 и amount=300, а в столбец client пишет NaN — потому что значение из правой таблицы не пришло:
left = pd.merge(orders, clients, on='order_id', how='left')
print(left)
# order_id amount client
# 0 1 100 Аня
# 1 2 200 Боря
# 2 3 300 NaN
NaN появляется именно в столбцах, пришедших из правого DataFrame (client). Столбцы левого (order_id, amount) остаются нетронутыми.
При right join логика зеркальная: все строки правого DataFrame (clients) сохраняются. order_id=4 есть в clients, но не в orders — значит, amount получит NaN:
right = pd.merge(orders, clients, on='order_id', how='right')
print(right)
# order_id amount client
# 0 1 100.0 Аня
# 1 2 200.0 Боря
# 2 4 NaN Вера
Здесь стоит обратить внимание на dtype: amount был целочисленным, но после right join pandas переводит его в float64, чтобы вместить NaN — обычный int в pandas не умеет хранить пропуски.
При outer join строки без пары сохраняются с обеих сторон одновременно. order_id=3 даёт NaN в client, order_id=4 даёт NaN в amount:
outer = pd.merge(orders, clients, on='order_id', how='outer')
print(outer)
# order_id amount client
# 0 1 100.0 Аня
# 1 2 200.0 Боря
# 2 3 300.0 NaN
# 3 4 NaN Вера
Четыре строки: две совпавших без пропусков, одна с NaN в client, одна с NaN в amount.
inner join — единственный режим, который не добавляет NaN из-за несовпадения ключей: в результат попадают только строки, нашедшие пару в обоих DataFrame. Однако если в исходных данных уже были пропуски, inner join их сохранит:
orders_with_nan = pd.DataFrame({'order_id': [1, 2], 'amount': [100, None]})
inner = pd.merge(orders_with_nan, clients, on='order_id', how='inner')
print(inner)
# order_id amount client
# 0 1 100.0 Аня
# 1 2 NaN Боря
НaN в amount здесь пришёл из исходной таблицы — не от несовпадения ключей. Это важно понимать: inner join гарантирует только то, что все ключи совпали, но не то, что все поля заполнены.
Практическое правило: сразу после merge с how='left' или 'outer' стоит проверить, сколько NaN появилось в ключевых столбцах:
print(left['client'].isna().sum()) # 1 — один заказ без клиента
print(outer['amount'].isna().sum()) # 1 — один клиент без заказа
Эти числа говорят о несовпадениях в данных — и следующий шаг зависит от задачи: либо заполнить пропуски, либо разобраться, почему строки не нашли пару.
