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

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 — один клиент без заказа

Эти числа говорят о несовпадениях в данных — и следующий шаг зависит от задачи: либо заполнить пропуски, либо разобраться, почему строки не нашли пару.