2025, Oct 20 08:17

Объединяем два CSV в pandas: левый merge и N/A вместо NaN

Пошагово покажем, как объединить CSV в pandas по product_code без потери строк: левый merge и fillna для N/A, разбор подводных камней, примеры кода и советы.

Объединяем два CSV в pandas без потери несовпавших строк: чистое левое объединение с заполнением N/A

Когда вы дополняете один набор данных атрибутами из другого, незаметная настройка по умолчанию в pandas может тихо убрать строки, которые вы рассчитывали сохранить. Задача здесь простая: соединить записи отгрузок с метаданными товаров по product_code, оставить все отгрузки даже при отсутствии товара и заполнить пропуски значением N/A.

Постановка задачи

Два простых табличных входа показывают ситуацию. В первой таблице — отгрузки, во второй — описания и категории товаров. Ожидаемый результат должен сохранить каждую отгрузку и добавить описание/категорию там, где есть совпадение.

shipment_id,product_code,quantity,date
S001,P123,10,2025-07-01
S002,P456,5,2025-07-02
S003,P789,8,2025-07-03
product_code,description,category
P123,Widget A,Tools
P456,Widget B,Hardware

Ожидаемый результат:

shipment_id,product_code,quantity,date,description,category
S001,P123,10,2025-07-01,Widget A,Tools
S002,P456,5,2025-07-02,Widget B,Hardware
S003,P789,8,2025-07-03,N/A,N/A

Минимальный пример, показывающий подводный камень

При использовании объединения по умолчанию отгрузка P789, не имеющая пары, пропадает — а этого в данном сценарии как раз нельзя допускать.

import pandas as pd
ship_data = [
    ["S001", "P123", 10, "2025-07-01"],
    ["S002", "P456", 5, "2025-07-02"],
    ["S003", "P789", 8, "2025-07-03"]
]
cols_ship = ["shipment_id", "product_code", "quantity", "date"]
frame_ship = pd.DataFrame(ship_data, columns=cols_ship)
item_data = [
    ["P123", "Widget A", "Tools"],
    ["P456", "Widget B", "Hardware"]
]
cols_item = ["product_code", "description", "category"]
frame_item = pd.DataFrame(item_data, columns=cols_item)
# Объединение по умолчанию: строки без совпадений удаляются
inner_joined = pd.merge(frame_ship, frame_item)
print(inner_joined)

Что на самом деле происходит

Поведение pandas.merge по умолчанию исключает строки, которым не нашлось пары во втором DataFrame. Иначе говоря, несовпавшие записи отбрасываются, если вы явно не попросите иное. При переключении на левое объединение pandas сохраняет все строки из левого DataFrame; для тех, что не нашли соответствия справа, в новые столбцы помещается NaN. После этого вы можете заменить эти NaN на N/A, чтобы получить требуемый формат.

Есть и тонкость. Если в обоих DataFrame присутствуют несколько одноимённых столбцов, pandas попытается объединять по всем. Чтобы закрепиться на конкретном ключе, передайте его явно через параметры left_on и right_on.

Решение: левое объединение + fillna

Исправление короткое: выполните левый merge по product_code, затем заполните пропуски значением N/A.

import pandas as pd
ship_data = [
    ["S001", "P123", 10, "2025-07-01"],
    ["S002", "P456", 5, "2025-07-02"],
    ["S003", "P789", 8, "2025-07-03"]
]
cols_ship = ["shipment_id", "product_code", "quantity", "date"]
frame_ship = pd.DataFrame(ship_data, columns=cols_ship)
item_data = [
    ["P123", "Widget A", "Tools"],
    ["P456", "Widget B", "Hardware"]
]
cols_item = ["product_code", "description", "category"]
frame_item = pd.DataFrame(item_data, columns=cols_item)
# Верно: сохранить все отгрузки и заполнить несовпадения
result_all_cols = (
    pd.merge(frame_ship, frame_item, how="left")
      .fillna("N/A")
)
print(result_all_cols)

Если вам нужна только категория из таблицы товаров, объединяйте с подмножеством столбцов. Так может быть удобнее, так как это не меняет исходный DataFrame frame_item.

result_category_only = (
    pd.merge(frame_ship, frame_item[["product_code", "category"]], how="left")
      .fillna("N/A")
)
print(result_category_only)

Если в ваших DataFrame есть несколько одноимённых столбцов и объединять нужно по конкретному, укажите его явно.

result_explicit_key = (
    pd.merge(
        frame_ship,
        frame_item,
        how="left",
        left_on="product_code",
        right_on="product_code"
    ).fillna("N/A")
)

Почему это важно

Обогащение данных должно быть добавочным процессом. Случайное исключение отгрузок из-за отсутствующего товара сводит усилия на нет и искажает последующую отчётность. Левое объединение сохраняет весь массив отгрузок и явно показывает отсутствие через N/A — это прозрачнее и проще для аудита.

Итоги

Когда вы дополняете записи одной таблицы атрибутами из другой, выбирайте левый merge, чтобы сохранить все строки источника, и сразу стандартизируйте пропуски через fillna("N/A"). Если по обе стороны есть несколько одноимённых столбцов, зафиксируйте ключ объединения с помощью left_on и right_on. А когда нужен лишь набор столбцов из правой таблицы, объединяйте именно с этим подмножеством — так результат будет аккуратнее.

Статья основана на вопросе с StackOverflow от user21677098 и ответе Ranger.