Урок курса

merge и concat: соединение таблиц с контролем кардинальности

NumPy и pandas: практический тренажёр

В прошлом уроке transform помогал добавлять к каждой строке групповой показатель — сумму или среднее — не выходя за пределы одного DataFrame. Но если нужные данные хранятся в двух разных таблицах, transform не поможет: нужна операция явного соединения. Именно её разберём сейчас.

pd.merge: параметры on и how — механика четырёх типов соединения

pd.merge — основная функция pandas для соединения двух DataFrame по общему ключевому столбцу. Минимальный вызов выглядит так:

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': ['Аня', 'Боря', 'Вера']})

result = pd.merge(orders, clients, on='order_id', how='inner')
print(result)
#    order_id  amount client
# 0         1     100    Аня
# 1         2     200   Боря

Параметр on указывает имя столбца, по которому идёт сравнение. Он должен присутствовать в обоих DataFrame. Параметр how задаёт правило отбора строк в результат — и именно здесь поведение четырёх режимов принципиально расходится.

inner — строгое пересечение. В результат попадают только те строки, у которых значение ключа есть в обеих таблицах. В примере выше order_id=3 нет в clients, order_id=4 нет в orders — обе строки отброшены.

left — все строки левого DataFrame сохраняются. Если для какого-то order_id из orders нет совпадения в clients, строка всё равно попадает в результат.

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
print(left.shape)  # (3, 3)

order_id=3 присутствует, но client для него — NaN, потому что в правой таблице такой записи нет.

right — симметрично left: все строки правого DataFrame (clients) сохраняются, строки левого (orders) добавляются там, где есть совпадение.

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   Вера
print(right.shape)  # (3, 3)

order_id=4 есть только в clients, поэтому amount для него — NaN.

outer — объединение всех строк из обоих DataFrame. Строки без пары получают NaN в столбцах противоположной таблицы.

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   Вера
print(outer.shape)  # (4, 3)

Четыре строки: две совпавших, одна только из orders, одна только из clients.

Параметр on принимает не только строку, но и список столбцов, если ключ составной: on=['user_id', 'date']. Главное требование — все перечисленные столбцы должны существовать в обоих DataFrame с одинаковыми именами.

Выбор how — это не технический, а смысловой вопрос. Он зависит от того, какие строки нельзя потерять: если приоритет у левой таблицы (основная сущность) — left; если нужно полное покрытие обеих — outer; если важны только подтверждённые совпадения — inner.

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

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

suffixes, indicator=True и validate: контроль качества соединения

После того как разобрались с тем, какие строки попадают в результат и откуда берутся NaN, следующий вопрос — как убедиться, что merge сделал именно то, что ожидалось. Для этого в pd.merge есть три параметра: suffixes, indicator и validate.

Конфликт имён столбцов и suffixes

Если оба DataFrame содержат столбец с одинаковым именем, и этот столбец не является ключом соединения, pandas не выбросит ошибку — он переименует оба, добавив суффиксы _x и _y:

import pandas as pd

orders = pd.DataFrame({
    'order_id': [1, 2, 3],
    'amount':   [100, 200, 300],
    'status':   ['new', 'paid', 'new']
})
reviews = pd.DataFrame({
    'order_id': [1, 2],
    'status':   ['ok', 'ok'],
    'score':    [4.5, 3.8]
})

result = pd.merge(orders, reviews, on='order_id', how='left')
print(result.columns.tolist())
# ['order_id', 'amount', 'status_x', 'status_y', 'score']

Столбец status оказался в обеих таблицах, и pandas создал status_x (из orders) и status_y (из reviews). Это работает, но читать такой код через неделю неудобно. Параметр suffixes позволяет задать понятные имена явно:

result = pd.merge(
    orders, reviews,
    on='order_id',
    how='left',
    suffixes=('_order', '_review')
)
print(result.columns.tolist())
# ['order_id', 'amount', 'status_order', 'status_review', 'score']

Теперь сразу понятно, откуда пришёл каждый status.

indicator=True: аудит происхождения строк

Параметр indicator=True добавляет в результат столбец _merge, который показывает, из какого источника пришла каждая строка. Возможные значения: 'both' — строка нашла пару в обоих DataFrame, 'left_only' — строка есть только в левом, 'right_only' — только в правом.

orders = pd.DataFrame({'order_id': [1, 2, 3], 'amount': [100, 200, 300]})
clients = pd.DataFrame({'order_id': [1, 2, 4], 'client': ['Аня', 'Боря', 'Вера']})

outer = pd.merge(orders, clients, on='order_id', how='outer', indicator=True)
print(outer)
#    order_id  amount client      _merge
# 0         1   100.0    Аня        both
# 1         2   200.0   Боря        both
# 2         3   300.0    NaN   left_only
# 3         4     NaN   Вера  right_only

Столбец _merge имеет категориальный dtype. Чаще всего его используют сразу после merge, чтобы посмотреть, сколько строк осталось без пары:

print(outer['_merge'].value_counts())
# both          2
# left_only     1
# right_only    1

Это удобнее, чем считать NaN вручную: indicator=True явно разделяет случай «строка без пары слева» и «строка без пары справа», а не просто фиксирует факт пропуска.

validate: проверка кардинальности до использования результата

Параметр validate позволяет задекларировать ожидаемую структуру соединения. Если реальные данные её нарушают, pandas выбрасывает MergeError — сразу, до того как результат будет где-то использован.

pandas принимает четыре значения: 'one_to_one', 'one_to_many', 'many_to_one' и 'many_to_many'. В этом уроке используем три проверяющих контракта, которые реально ограничивают уникальность ключей:

  • 'one_to_one' — ключ уникален в обоих DataFrame
  • 'one_to_many' — ключ уникален в левом, может повторяться в правом
  • 'many_to_one' — ключ может повторяться в левом, уникален в правом

'many_to_many' тоже допустимо, но не проверяет уникальность ни в одной из сторон — оно лишь документирует намерение, не защищая от неожиданного декартова произведения.

Посмотрим, как работает проверка, когда ожидание не совпадает с реальностью:

orders_dup = pd.DataFrame({
    'order_id': [1, 1, 2],   # order_id=1 дублируется
    'amount':   [100, 150, 200]
})
clients = pd.DataFrame({'order_id': [1, 2, 4], 'client': ['Аня', 'Боря', 'Вера']})

try:
    result = pd.merge(
        orders_dup, clients,
        on='order_id',
        how='inner',
        validate='one_to_one'
    )
except pd.errors.MergeError as e:
    print(e)
# Merge keys are not unique in left dataset; not a one-to-one merge

Если бы не было validate, merge молча выполнился бы и вернул три строки вместо двух — order_id=1 объединился бы с Аней дважды. Это декартово произведение по ключу, которое легко принять за корректный результат.

Когда дубликаты по ключу ожидаемы (например, один клиент — много заказов), используют 'many_to_one':

result = pd.merge(
    orders_dup, clients,
    on='order_id',
    how='inner',
    validate='many_to_one'
)
print(result.shape)  # (3, 3) — корректно, validate не возражает

В практике validate особенно важен при соединении со справочниками: если справочник должен быть уникален по ключу, validate='many_to_one' это гарантирует и сразу сообщает о проблеме, если в справочнике завелись дубликаты.

pd.concat с ignore_index=True: добавление строк без соединения по ключу

pd.merge работает по ключу — он ищет совпадения между двумя таблицами. pd.concat делает принципиально другое: он просто складывает строки одна под другой, без какого-либо поиска пар.

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

import pandas as pd

orders_old = pd.DataFrame({
    'order_id': [1, 2, 3],
    'amount':   [100, 200, 300]
})

orders_new = pd.DataFrame({
    'order_id': [4, 5],
    'amount':   [400, 500]
})

all_orders = pd.concat([orders_old, orders_new])
print(all_orders)
#    order_id  amount
# 0         1     100
# 1         2     200
# 2         3     300
# 0         4     400
# 1         5     500

Результат правильный по данным, но индекс сломан: значения 0 и 1 повторяются. Это потому что pd.concat по умолчанию сохраняет исходные индексы обоих DataFrame. Если исходные индексы не несут смысловой нагрузки, нужно передать ignore_index=True:

all_orders = pd.concat([orders_old, orders_new], ignore_index=True)
print(all_orders)
#    order_id  amount
# 0         1     100
# 1         2     200
# 2         3     300
# 3         4     400
# 4         5     500

Теперь индекс — сплошной RangeIndex от 0, как у свежесозданного DataFrame.

Несовпадение схем столбцов

Если у двух DataFrame разные наборы столбцов, pd.concat не выбросит ошибку. Он создаст объединённую схему, а в строках, где столбца не было, поставит NaN:

orders_with_note = pd.DataFrame({
    'order_id': [6],
    'amount':   [600],
    'note':     ['срочно']
})

all_orders_with_note = pd.concat([orders_old, orders_with_note], ignore_index=True)
print(all_orders_with_note)
#    order_id  amount    note
# 0         1     100     NaN
# 1         2     200     NaN
# 2         3     300     NaN
# 3         6     600  срочно

Столбец note появился из второго DataFrame. Три строки из orders_old получили NaN в этом столбце — механика та же, что при left/outer join, но причина другая: не несовпадение ключей, а отсутствие столбца в одном из источников.

Это важно проверять после concat: если схемы таблиц расходятся неожиданно, NaN-столбцы сразу это покажут:

print(all_orders_with_note.isna().sum())
# order_id    0
# amount      0
# note        3

Три пропуска в note — три из четырёх строк пришли из таблицы без этого столбца.

Чем pd.concat отличается от merge

pd.concat не знает ничего о содержимом строк — он не проверяет ключи, не ищет совпадений и не контролирует кардинальность. Если передать два одинаковых DataFrame, он просто удвоит строки без предупреждения. Поэтому после concat стоит проверить, что итоговое число строк соответствует ожидаемому:

# all_orders = pd.concat([orders_old, orders_new], ignore_index=True)
expected = len(orders_old) + len(orders_new)
assert len(all_orders) == expected, f"Ожидалось {expected} строк, получено {len(all_orders)}"

Если нужно проверить уникальность ключа после объединения, используйте duplicated:

dupes = all_orders[all_orders.duplicated(subset=['order_id'])]
if not dupes.empty:
    print(f"Дубликаты order_id: {dupes['order_id'].tolist()}")

Что делать с дубликатами — зависит от спецификации задачи: нужно знать не только по какому столбцу проверять уникальность (subset), но и какую запись считать правильной (keep='first', keep='last' или иной способ разрешения). Без явного правила удалять строки не стоит: одинаковые записи могут быть корректными наблюдениями, а не ошибками.

Когда нужно добавить строки — pd.concat. Когда нужно обогатить таблицу данными из другой по общему ключу — pd.merge.

Закрепите тему на задачах

Перед merge проверьте уникальность ключей с обеих сторон. Рост числа строк может быть закономерным many-to-many соединением, а не просто дублями исходной таблицы.

pandas merge(how='left'): что будет без совпадения ключа

pandas merge: validate и suffixes при соединении таблиц

pandas merge indicator: right и outer join без потери строк

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

Задача 1

DataFrame orders (столбцы: order_id, amount) и DataFrame clients (столбцы: order_id, name) соединяются:
result = pd.merge(orders, clients, on='order_id', how='left')
В orders есть order_id=5, которого нет в clients. Что будет в result для строки с order_id=5 в столбце 'name'?

Выберите один вариант
Без регистрации · результат не сохраняется

Задача 2

Выполнен код:
import pandas as pd
from pandas.errors import MergeError
df1 = pd.DataFrame({'id': [1, 1, 2], 'val': [10, 20, 30]})
df2 = pd.DataFrame({'id': [1, 2], 'info': ['a', 'b']})
try:
result = pd.merge(df1, df2, on='id', how='inner', validate='many_to_one')
except MergeError as e:
print('error')
else:
print(result.shape[0])
Что будет выведено?

Выберите один вариант
Без регистрации · результат не сохраняется

Задача 3

Выполнен код:
import pandas as pd
a = pd.DataFrame({'k': [1, 2], 'v': [10, 20]})
b = pd.DataFrame({'k': [3, 4], 'v': [30, 40]})
result = pd.concat([a, b], ignore_index=True)
Чему равен result['k'].tolist()?

Выберите один вариант
Без регистрации · результат не сохраняется

Задача 4

Склад и поставки: три типа join, indicator и concat за один проход

Что уже дано

Проверяющая система запускает вашу программу и подаёт данные через stdin. Никаких заранее созданных переменных нет — вы читаете всё сами.

Что нужно сделать

Напишите программу, которая:

  1. Читает из stdin целое число p — количество строк таблицы товаров.
  2. Читает следующие p строк; каждая содержит два токена через пробел: item_id (целое число) и stock (целое число). Создаёт pd.DataFrame с именем items и столбцами 'item_id', 'stock'.
  3. Читает целое число s — количество строк таблицы поставок.
  4. Читает следующие s строк; каждая содержит два токена через пробел: item_id (целое число) и supply (целое число). Создаёт pd.DataFrame с именем supplies и столбцами 'item_id', 'supply'.
  5. Шаг A. inner join. Выполняет pd.merge(items, supplies, on='item_id', how='inner'). Выводит одну строку: inner shape=<shape>.
  6. Шаг B. left join. Выполняет pd.merge(items, supplies, on='item_id', how='left'). Выводит одну строку: left nan_supply=<k>, где <k> — число NaN в столбце 'supply'.
  7. Шаг C. outer join с indicator. Выполняет pd.merge(items, supplies, on='item_id', how='outer', indicator=True). Выводит одну строку: outer left_only=<a> right_only=<b> both=<c>, где <a>, <b>, <c> — число строк с соответствующим значением '_merge'.
  8. Шаг D. concat. Читает целое число e — количество строк дополнительных товаров. Читает следующие e строк; каждая содержит два токена: item_id (целое число) и stock (целое число). Создаёт pd.DataFrame extra с теми же столбцами. Объединяет items и extra через pd.concat([items, extra], ignore_index=True). Выводит одну строку: concat shape=<shape>.

Ввод и вывод

Программа читает stdin и пишет в stdout.

Ограничения: p >= 1, s >= 1.

Формат ввода

p
item_id stock
...
s
item_id supply
...
e
item_id stock
...

Формат вывода

Ровно четыре строки в порядке шагов A–D:

inner shape=(<r>, <c>)
left nan_supply=<k>
outer left_only=<a> right_only=<b> both=<c>
concat shape=(<r>, <c>)
РешениеPython

Без регистрации. Гостевая проверка не сохраняет прогресс. Для сохранения перейдите к уроку.

Отправьте решение, чтобы увидеть результаты тестов.

Задача 5

Суффиксы и validate: merge двух таблиц с общим столбцом-не-ключом

Что уже дано

Проверяющая система запускает вашу программу и подаёт данные через stdin. Никаких заранее созданных переменных нет — вы читаете всё сами.

Что нужно сделать

Напишите программу, которая работает с двумя таблицами: складом (warehouse) и поставщиком (supplier). Обе таблицы содержат столбец rating (помимо ключа), поэтому при merge появятся суффиксы.

  1. Читает из stdin целое число w — количество строк таблицы склада.
  2. Читает следующие w строк; каждая содержит три токена через пробел: item_id (целое число), stock (целое число), rating (вещественное число). Создаёт pd.DataFrame с именем warehouse и столбцами 'item_id', 'stock', 'rating'.
  3. Читает целое число s — количество строк таблицы поставщика.
  4. Читает следующие s строк; каждая содержит три токена через пробел: item_id (целое число), supply (целое число), rating (вещественное число). Создаёт pd.DataFrame с именем supplier и столбцами 'item_id', 'supply', 'rating'.
  5. Шаг A. outer join с явными суффиксами. Выполняет:
merged = pd.merge(warehouse, supplier, on='item_id', how='outer',
suffixes=('_wh', '_sup'))

Выводит одну строку: cols=<список имён столбцов> — list(merged.columns) как строку Python.
Затем выводит: nan_supply=<k> — число NaN в столбце 'supply'.
Затем выводит: nan_rating_sup=<k> — число NaN в столбце 'rating_sup'.
6. Шаг B. validate='one_to_one' — ожидаемая ошибка. Выполняет merge с теми же параметрами плюс validate='one_to_one' внутри try/except и при поимке pd.errors.MergeError выводит строку: validate=error.
7. Шаг C. validate='one_to_many' — успешное соединение. Выполняет inner join тех же таблиц с validate='one_to_many' и suffixes=('_wh', '_sup'). Выводит одну строку: validate=ok shape=<shape>.

Ввод и вывод

Программа читает stdin и пишет в stdout.

Ограничения: w >= 1, s >= 1.

Формат ввода

w
item_id stock rating
...
s
item_id supply rating
...

Гарантируется:

  • ключ item_id уникален в таблице warehouse (one side);
  • в таблице supplier хотя бы один item_id встречается более одного раза (many side), поэтому validate='one_to_one' обязан выбросить MergeError;
  • хотя бы один item_id из warehouse отсутствует в supplier (порождает NaN в supply и rating_sup при outer join);
  • хотя бы один item_id из supplier отсутствует в warehouse (порождает NaN в stock и rating_wh).

Формат вывода

Ровно пять строк:

cols=<list>
nan_supply=<k>
nan_rating_sup=<k>
validate=error
validate=ok shape=(<r>, <c>)
РешениеPython

Без регистрации. Гостевая проверка не сохраняет прогресс. Для сохранения перейдите к уроку.

Отправьте решение, чтобы увидеть результаты тестов.

Задача 6

Найди и исправь: три ошибки в pd.merge (how, indicator, pd.concat)

Что уже дано

Проверяющая система запускает вашу программу и подаёт данные через stdin. Никаких заранее созданных переменных нет.

В редакторе уже есть сломанный код. В нём ровно три ошибки в трёх независимых строках:

  1. Шаг A: pd.merge(base, updates, on='sku', how='inner') — используется inner, хотя нужно сохранить все строки левого DataFrame и добавить данные из правого там, где есть совпадение (how='left').
  2. Шаг B: pd.merge(base, updates, on='sku', how='outer') вызывается без indicator=True, хотя нужно добавить столбец '_merge' для диагностики происхождения строк.
  3. Шаг C: pd.concat([base, extra]) вызывается без ignore_index=True, хотя нужно получить сброшенный RangeIndex.

Каждая ошибка находится в отдельной строке кода. Исправьте все три.

Что нужно сделать

Исправьте три ошибки так, чтобы программа:

  1. Читала из stdin целое число b — количество строк базовой таблицы склада.
  2. Читала следующие b строк; каждая содержит два токена через пробел: sku (целое число) и qty (целое число). Создавала pd.DataFrame с именем base и столбцами 'sku', 'qty'.
  3. Читала целое число u — количество строк таблицы обновлений.
  4. Читала следующие u строк; каждая содержит два токена через пробел: sku (целое число) и new_qty (целое число). Создавала pd.DataFrame с именем updates и столбцами 'sku', 'new_qty'.
  5. Шаг A. left join. Выполняла pd.merge(base, updates, on='sku', how='left'). Выводила одну строку: left shape=<shape> и left nan=<k>, где <k> — число NaN в столбце 'new_qty'. Формат: left shape=(r, c) nan=k.
  6. Шаг B. outer join с indicator. Выполняла pd.merge(base, updates, on='sku', how='outer', indicator=True). Выводила одну строку: both=<a> left_only=<b> right_only=<c> — число строк с соответствующим значением '_merge'.
  7. Читала целое число e — количество строк дополнительных записей.
  8. Читала следующие e строк; каждая содержит два токена: sku (целое число) и qty (целое число). Создавала pd.DataFrame extra с теми же столбцами 'sku', 'qty'.
  9. Шаг C. concat с ignore_index. Выполняла pd.concat([base, extra], ignore_index=True). Выводила одну строку: concat shape=<shape> index_start=<first> index_end=<last>, где <first> и <last> — первый и последний элементы индекса результата.

Ввод и вывод

Программа читает stdin и пишет в stdout.

Ограничения: b >= 1, u >= 1.

Формат ввода

b
sku qty
...
u
sku new_qty
...
e
sku qty
...

Формат вывода

Ровно три строки:

left shape=(r, c) nan=k
both=a left_only=b right_only=c
concat shape=(r, c) index_start=0 index_end=last
РешениеPython

Без регистрации. Гостевая проверка не сохраняет прогресс. Для сохранения перейдите к уроку.

Отправьте решение, чтобы увидеть результаты тестов.

Задача 7

Right join и outer с indicator: диагностика несовпавших строк

Что уже дано

Проверяющая система запускает вашу программу и подаёт данные через stdin. Никаких заранее созданных переменных нет — вы читаете всё сами.

Что нужно сделать

Напишите программу, которая работает с двумя таблицами — курсов и записей студентов.

  1. Читает из stdin целое число c — количество строк таблицы курсов.
  2. Читает следующие c строк; каждая содержит два токена через пробел: course_id (целое число) и title (строка без пробелов). Создаёт pd.DataFrame с именем courses и столбцами 'course_id', 'title'.
  3. Читает целое число r — количество строк таблицы записей.
  4. Читает следующие r строк; каждая содержит два токена через пробел: course_id (целое число) и student (строка без пробелов). Создаёт pd.DataFrame с именем registrations и столбцами 'course_id', 'student'.
  5. Шаг A. right join. Выполняет pd.merge(courses, registrations, on='course_id', how='right'). Выводит одну строку: right shape=<shape>, затем одну строку: right nan_title=<k> — число NaN в столбце 'title' результата.
  6. Шаг B. outer join с indicator. Выполняет pd.merge(courses, registrations, on='course_id', how='outer', indicator=True). Выводит одну строку: outer both=<a> left_only=<b> right_only=<c> — число строк с каждым значением '_merge'.
  7. Шаг C. NaN по оси right. В результате outer join из шага B подсчитывает число NaN в столбце 'student' и выводит одну строку: outer nan_student=<k>.

Ввод и вывод

Программа читает stdin и пишет в stdout.

Ограничения: c >= 1, r >= 1.

Формат ввода

c
course_id title
...
r
course_id student
...

Формат вывода

Ровно четыре строки в порядке шагов A–C:

right shape=(<rows>, <cols>)
right nan_title=<k>
outer both=<a> left_only=<b> right_only=<c>
outer nan_student=<k>
РешениеPython

Без регистрации. Гостевая проверка не сохраняет прогресс. Для сохранения перейдите к уроку.

Отправьте решение, чтобы увидеть результаты тестов.

Задачи к уроку

Все задачи курса →

Продолжить с сохранением прогресса

Перейдите к уроку в учебном кабинете.

Перейти к интерактивному уроку