Данные почти никогда не лежат в одной таблице: заказы отдельно, клиенты отдельно, справочник городов отдельно. Склеить их правильно и не размножить строки по дороге: одна из главных проверок на собесе аналитика.
merge: соединение по колонкам
pd.merge(left, right, on=..., how=...) работает как SQL JOIN.
import pandas as pd
orders = pd.DataFrame({'order_id': [1, 2, 3], 'user_id': [10, 20, 30], 'amount': [500, 300, 900]})
users = pd.DataFrame({'user_id': [10, 20, 40], 'city': ['Москва', 'Казань', 'Пермь']})
orders.merge(users, on='user_id', how='left')
# order_id user_id amount city
# 0 1 10 500 Москва
# 1 2 20 300 Казань
# 2 3 30 900 NaN
Значения how:
| how | Что остаётся |
|---|---|
inner (по умолчанию) | только ключи, которые есть в обеих таблицах |
left | все строки левой таблицы |
right | все строки правой |
outer | все ключи из обеих |
cross | декартово произведение, ключ не нужен |
Если ключи называются по-разному, используйте left_on и right_on. Если ключ в индексе, есть left_index=True и right_index=True.
Размножение строк
Если в правой таблице ключ повторяется, каждая строка слева склеится с каждым совпадением справа. Было 1000 заказов, после merge со справочником, где у клиента две записи (например, два адреса), стало 1300. Суммы выручки после этого завышены.
Защита: параметр validate.
orders.merge(users, on='user_id', how='left', validate='many_to_one')
Если в users ключ не уникален, pandas выбросит MergeError. Варианты: 'one_to_one', 'one_to_many', 'many_to_one', 'many_to_many'. Хорошая привычка: всегда проверять число строк до и после соединения.
indicator: откуда пришла строка
m = orders.merge(users, on='user_id', how='outer', indicator=True)
m['_merge'].value_counts()
# both, left_only, right_only
Так удобно искать «заказы без клиента» или «клиентов без заказов». Анти-джойн (строки слева без пары справа) делают так: outer или left с indicator=True, потом фильтр _merge == 'left_only'. Ещё проще для одного ключа: orders[~orders['user_id'].isin(users['user_id'])].
Одинаковые имена колонок
Если в обеих таблицах есть колонка name, которая не входит в ключ, pandas добавит суффиксы name_x и name_y. Их можно задать: suffixes=('_order', '_user').
Типы ключей
Ключ user_id в одной таблице int64, а в другой строка '10': pandas не приведёт их сам. В современных версиях вы получите ошибку о несовместимых типах ключей. Если оба ключа строки, но с пробелами или в разном регистре, соединение молча вернёт пустоту. Перед merge проверьте dtypes и почистите ключи.
join: соединение по индексу
left.join(right) по умолчанию соединяет по индексу и делает left join. Удобно, когда справочник уже проиндексирован ключом:
users_idx = users.set_index('user_id')
orders.join(users_idx, on='user_id')
join это обёртка над merge с другими умолчаниями, принципиально нового он не делает.
concat: склейка по оси
pd.concat не ищет совпадений по ключу, он ставит таблицы друг под друга (axis=0) или рядом (axis=1, с выравниванием по индексу).
jan = pd.DataFrame({'user_id': [1, 2], 'amount': [10, 20]})
feb = pd.DataFrame({'user_id': [3], 'amount': [30]})
pd.concat([jan, feb], ignore_index=True)
Без ignore_index=True индексы сохранятся и повторятся: 0, 1, 0. Колонки, которых нет в одной из таблиц, заполнятся NaN.
Добавлять строки по одной в цикле через concat медленно: каждый вызов копирует всё. Соберите список словарей или DataFrame и сделайте один concat в конце. Метода DataFrame.append в pandas 2.0+ больше нет.
merge_asof: ближайшее по времени
Когда нужно найти для каждого события последний известный курс или статус на момент события, обычный merge не подходит: точного совпадения времени нет. Для этого есть pd.merge_asof(left, right, on='ts', by='user_id'), обе таблицы должны быть отсортированы по on.
Частые ошибки
- Не проверили уникальность ключа справа, и строки размножились.
innerпо умолчанию: молча потеряли заказы без клиента.- Фильтр по колонке правой таблицы после
leftmerge выкинул строки с NaN, и left фактически стал inner. - Разные типы ключей или грязные строки.
concatв цикле.
Что спросят на собесе
- Чем отличаются
inner,left,outer,cross? - Почему после
mergeстрок стало больше и как это предотвратить? - Как найти строки левой таблицы без пары в правой?
- Чем
merge,joinиconcatотличаются друг от друга? - Как соединить события с последним известным значением по времени?