Прямой ответ
Три правила делают джойны в pandas безопасными. Первое: типы ключей совпадают до merge — ключ int64 против object/str не соединится (или, хуже, молча даст пустой результат в старых сценариях): приведение astype до, а не расследование после. Второе: validate= всегда — validate="m:1"/"1:1" превращает незамеченные дубликаты ключа (и декартово произведение, раздувающее таблицу в десятки раз) в явную ошибку. Третье: проверка результата — число строк и indicator=True (откуда приехала каждая строка) после каждого слияния; «merge прошёл без ошибок» не означает «merge сделал то, что вы думали».
Виды соединений: семантика, а не синтаксис
inner — только совпавшие ключи (теряются unmatched с обеих сторон — для «показать общие» правильно, для «обогатить основную таблицу» — тихая потеря строк). left — все строки левой + совпадения из правой (основной паттерн обогащения: справочник присоединяется к фактам). right — то же зеркально (удобнее поменять кадры местами и взять left — читается легче). outer — объединение ключей (для сверки двух источников, где пропуски с обеих сторон осмысленны). Выбор вида — содержательное решение: «какие строки я готов потерять» — и оно принимается до написания кода. Отдельные случаи: join (по индексу — удобен, когда ключ уже индекс), concat (стопка таблиц, не соединение по ключу), merge_asof (ближайшее по времени — для рядов с несовпадающими таймстампами; корректность дат — из статьи про datetime).
Ловушка ключей: типы, пробелы, дубликаты
Невидимые несовпадения — главный источник «пустых колонок после merge»: 1 (int) против "1" (str) — разные ключи; "Москва " с хвостовым пробелом — тоже (строковая гигиена — в очистке данных); NaN в ключе не матчится ни с чем (даже с NaN — семантика SQL NULL). Дубликаты ключа в «справочной» стороне — вторая классика: merge по неуникальному ключу умножает строки (10 заказов × 2 записи клиента = 20 строк «как бы заказов» — и сумма по таблице удвоилась). Механика проверки до merge: df.duplicated(subset=["ключ"]).sum() на обеих сторонах; лечится дедупликацией с осознанным правилом («последняя запись по дате») или агрегацией справочника до уникальности. validate="m:1" делает эту проверку автоматической.
Декартово произведение: как раздуть таблицу в 100 раз
Частный случай предыдущего: ключ неуникален с обеих сторон → m:n-слияние → строки перемножаются. Симптом — размер результата резко больше обоих входов, суммы/средние «поплыли». Защита та же (validate не даст m:n молча), плюс привычка смотреть на len(result) против ожиданий сразу после merge. Отдельный подвид — merge по «плохому» ключу с низкой кардинальностью (пол, страна): соединять по нему факты с фактами почти всегда ошибка проектирования запроса (нужен join через сущность или агрегация до).
suffixes, индикатор и отладка
Одноимённые колонки с обеих сторон (кроме ключа) получают суффиксы _x/_y по умолчанию — бесполезные имена, в которых путаются через неделю; задавайте осмысленные (suffixes=("_факт", "_план")) или переименовывайте до merge. indicator=True добавляет колонку _merge (left_only/right_only/both) — для сверки источников и поиска «потерявшихся» строк это первый инструмент; для отладки — срез result[result["_merge"] == "left_only"] показывает, что именно не сматчилось. Полезная гигиена: how и on указывать явными параметрами всегда (дефолт inner по пересечению имён колонок — «удобная» магия, которая соединяет не то при случайном совпадении имён).
Практические паттерны
Обогащение фактов справочником: left-merge по уникальному ключу справочника + validate="m:1" + проверка result["справочник_col"].isna().mean() (доля несматчившихся — ожидаема ли). Сверка двух выгрузок: outer-merge + indicator + сравнение общих колонок (result.query("_merge == 'both' and сумма_x != сумма_y")). Агрегат вместо джойна: часто «присоединить таблицу» на деле означает «присоединить агрегат таблицы» — groupby до merge (агрегации) уменьшает и ключ, и размер. Джойны на больших файлах: фильтрация колонок/строк до слияния, типы ключей — узкие (int32/int64), и если не влезает — SQL-движок поверх файлов (DuckDB делает джойны по parquet без загрузки в pandas).
Практический сценарий: обогащение заказов справочником клиентов
Типовая задача: к таблице заказов (миллион строк) присоединить сегмент и город клиента из справочника. Правильная последовательность — не «merge и посмотреть», а четыре шага. Подготовка ключей: в заказах client_id приехал из CSV строкой с ведущими нулями, в справочнике — int64; приводим оба к одному типу (astype("int64") после проверки, что мусора нет) — иначе результат будет пустым, и pandas об этом промолчит. Проверка уникальности: clients.duplicated(subset=["id"]).sum() — ноль (справочник обязан быть уникален; если дубликаты есть — решаем, какая запись истинная, до merge, а не после раздувания таблицы). Слияние с гарантиями: orders.merge(clients[["id", "segment", "city"]], left_on="client_id", right_on="id", how="left", validate="m:1", indicator=True) — validate падает при нарушении «m:1» (защита от будущего невидимого дубля), indicator помечает источник каждой строки. Аудит результата: len(result) == len(orders) (left-merge не должен менять число строк — изменилось, значит validate не сработал и что-то пошло не так) и доля _merge == "left_only" — пять процентов заказов без клиента в справочнике; это не ошибка merge, а находка о данных — список таких client_id уходит на разбор. Четыре шага заняли минуту; пропущенные — превратились бы в «почему сумма выручки выросла вдвое» на завтрашней планёрке.
FAQ
merge или join — что использовать? merge — общий случай (по колонкам, любые how); join — частный (по индексу правой). Если ключ логически главный — set_index + join короче; иначе merge с явными on/left_on/right_on.
Почему после merge пропали строки? Почти всегда inner вместо left (unmatched отброшены) или merge по несовпавшим типам/значениям ключа (всё стало unmatched). Диагностика — indicator=True и сравнение len до/после.
Как соединить по нескольким колонкам?
on=["a", "b"] — кортеж ключей; composite-ключ чувствителен к типам обеих колонок и к NaN в любой из них; иногда удобнее склеить ключ в одну колонку (явно, с разделителем, которого нет в данных).
Что быстрее: merge или map?
Для «присоединить одну колонку по уникальному ключу» — map/set_index+loc быстрее и проще merge (и автоматически m:1). merge нужен, когда колонок несколько или ключ составной/неуникальный осознанно.
Источники: руководство pandas по merging и API DataFrame.merge — ссылки в шапке.