Урок курса

Дубли: duplicated и drop_duplicates по ключевым столбцам

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

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

duplicated: обнаружение дублирующихся строк по subset

Метод df.duplicated() возвращает булев Series той же длины, что и исходный DataFrame. Каждое значение отвечает на вопрос: «эта строка уже встречалась выше?» Если встречалась — True, если нет — False.

По умолчанию duplicated() сравнивает сразу все столбцы. Но на практике чаще нужно проверять дублирование только по ключевым полям — например, по паре (user_id, action). Для этого есть параметр subset:

import pandas as pd

df = pd.DataFrame({
    'user_id': [1, 2, 1, 3, 2],
    'action':  ['login', 'buy', 'login', 'view', 'buy'],
    'amount':  [0, 100, 0, 50, 200]
})

mask = df.duplicated(subset=['user_id', 'action'])
print(mask)
# 0    False
# 1    False
# 2     True
# 3    False
# 4     True
# dtype: bool

print(mask.sum())  # 2 — сколько строк являются кандидатами в дубли

Строки с индексами 2 и 4 отмечены True: пара (1, 'login') уже встречалась в строке 0, а пара (2, 'buy') — в строке 1. Столбец amount в сравнение не входит — только то, что указано в subset.

Важно понимать, что duplicated(subset=...) фиксирует совпадение значений по выбранным столбцам — и ничего больше. Это не означает, что строки бизнес-эквивалентны. Посмотри на пару (2, 'buy'): строки 1 и 4 совпадают по user_id и action, но amount у них разный — 100 и 200. Это могут быть два разных события одного пользователя, а не случайный дубль. Поэтому результат duplicated() — это список кандидатов в дубли по выбранному ключу, которых нужно осмыслить, прежде чем удалять.

Булев Series из duplicated() — это обычная маска. Её можно передать в df[mask], чтобы посмотреть на строки-кандидаты:

print(df[mask])
#    user_id action  amount
# 2        1  login       0
# 4        2    buy     200

Прежде чем переходить к удалению, стоит взглянуть и на «оригиналы» рядом — например, через keep=False (это разберём в следующем разделе). Убедившись, что совпадение ключа действительно означает нежелательный повтор, а не два легитимных события, можно переходить к drop_duplicates().

Параметр keep: управление тем, что считается дублем

Когда duplicated() находит группу строк с одинаковыми значениями по subset, она должна решить: какую из них считать «оригиналом», а какие — дублями. Этим управляет параметр keep.

По умолчанию keep='first': первое вхождение получает False (не дубль), все последующие — True. При keep='last' логика зеркальная: последнее вхождение считается «оригиналом», все предыдущие помечаются True.

import pandas as pd

df = pd.DataFrame({
    'user_id': [1, 2, 1, 3, 2],
    'action':  ['login', 'buy', 'login', 'view', 'buy'],
    'amount':  [0, 100, 0, 50, 200]
})

key_cols = ['user_id', 'action']

print(df.duplicated(subset=key_cols, keep='first').tolist())
# [False, False, True, False, True]

print(df.duplicated(subset=key_cols, keep='last').tolist())
# [True, True, False, False, False]

Здесь строки 0 и 2 — это пара (1, 'login'). При keep='first' дублем считается строка 2; при keep='last' — строка 0. Строки 1 и 4 — пара (2, 'buy'): аналогично меняется, кто из них получает True.

Третье значение — keep=False — отличается принципиально: дублями помечаются все строки группы, включая первое вхождение.

print(df.duplicated(subset=key_cols, keep=False).tolist())
# [True, True, True, False, True]

Это удобно для диагностики: чтобы увидеть все строки, участвующие в дублировании, достаточно отфильтровать по этой маске:

print(df[df.duplicated(subset=key_cols, keep=False)])
#    user_id action  amount
# 0        1  login       0
# 1        2    buy     100
# 2        1  login       0
# 4        2    buy     200

Один из важных практических нюансов: если передать keep=False не в duplicated(), а в drop_duplicates(), результат будет удалять все строки с дублями — включая «оригиналы». То есть из таблицы исчезнут все четыре строки выше, а не только «лишние» копии. Для большинства задач это не то, что нужно.

Правило простое: keep=False — инструмент диагностики, чтобы посмотреть на проблему. Для реального удаления используй keep='first' или keep='last', явно выбирая, какую запись из группы сохранить.

drop_duplicates: удаление дублей с явным subset и keep

Когда duplicated() показал группы совпадений по ключу и вы убедились, что этот ключ действительно должен быть уникален, можно переходить к удалению. drop_duplicates() возвращает новый DataFrame, в котором из каждой группы совпадений остаётся ровно одна строка — в соответствии с выбранным keep.

Синтаксис симметричен duplicated(): те же параметры subset и keep, тот же смысл:

import pandas as pd

df = pd.DataFrame({
    'user_id': [1, 2, 1, 3, 2],
    'action':  ['login', 'buy', 'login', 'view', 'buy'],
    'amount':  [0, 100, 0, 50, 200]
})

key_cols = ['user_id', 'action']

# keep='first' — оставить первое вхождение каждой группы
df_first = df.drop_duplicates(subset=key_cols, keep='first')
print(df_first[['user_id', 'action', 'amount']].values.tolist())
# [[1, 'login', 0], [2, 'buy', 100], [3, 'view', 50]]

# keep='last' — оставить последнее вхождение
df_last = df.drop_duplicates(subset=key_cols, keep='last')
print(df_last[['user_id', 'action', 'amount']].values.tolist())
# [[1, 'login', 0], [3, 'view', 50], [2, 'buy', 200]]

Разница между двумя вариантами наиболее заметна по паре (2, 'buy'): при keep='first' остаётся строка с amount=100 (индекс 1), при keep='last' — строка с amount=200 (индекс 4). Для user_id=1 результат внешне одинаков в обоих случаях — не потому что у этого пользователя нет дублей, а потому что оба вхождения пары (1, 'login') полностью идентичны, включая amount=0: неважно, какое из них сохранить. Строка user_id=3 не имеет совпадений по ключу и присутствует в результате без изменений.

drop_duplicates() не меняет исходный DataFrame. Результат — новый объект, его нужно присвоить переменной. Вызов без присвоения ничего не изменит.

Выбор между keep='first' и keep='last' определяется логикой данных: если строки упорядочены по времени и нужна самая свежая запись — берёшь keep='last'; если важна первая зафиксированная — keep='first'. В обоих случаях нужно убедиться, что данные отсортированы так, как ожидается, иначе «первое» и «последнее» вхождение окажутся случайными — и сохранится не та запись, что нужна.

Проверка результата дедупликации по shape и уникальности ключа

После drop_duplicates() стоит сделать две простые проверки, прежде чем двигаться дальше. Они занимают одну-две строки кода, но дают уверенность, что операция сработала именно так, как ожидалось.

Первая проверка — сравнить число строк до и после:

import pandas as pd

df = pd.DataFrame({
    'user_id': [1, 2, 1, 3, 2],
    'action':  ['login', 'buy', 'login', 'view', 'buy'],
    'amount':  [0, 100, 0, 50, 200]
})

key_cols = ['user_id', 'action']
df_clean = df.drop_duplicates(subset=key_cols, keep='first')

print(df.shape[0])       # 5 — исходное число строк
print(df_clean.shape[0]) # 3 — после дедупликации

Числа сошлись с ожиданием: было 5 строк, 2 из них — дубли, осталось 3. Если цифра неожиданная, это сигнал проверить subset — возможно, ключевые столбцы заданы не так, как нужно.

Вторая проверка — убедиться, что в очищенном DataFrame действительно нет дублей по ключевым столбцам:

print(df_clean.duplicated(subset=key_cols).sum())  # 0

Если результат не ноль, значит дедупликация не завершила работу — например, из-за опечатки в subset или неожиданного содержимого данных. Здесь используется тот же duplicated() с теми же key_cols, что и при диагностике — никаких новых инструментов.

Обе проверки вместе занимают буквально три строки и дают ответы на два разных вопроса:

  • shape[0] говорит о том, сколько строк удалено — структурный размер;
  • duplicated(...).sum() == 0 говорит о том, что уникальность по ключу достигнута.

Важно понимать, что эти проверки не замещают понимание семантики: они не скажут, правильную ли запись из группы дублей ты сохранил. Если нужна уверенность в содержании — смотри на конкретные строки через фильтрацию. Но структурные проверки выше стоит делать всегда, как финальный шаг дедупликации.

Попробуйте решить

DataFrame df содержит 8 строк. После вызова df.duplicated(subset=['city', 'date']).sum() получается 3. Сколько строк будет в df.drop_duplicates(subset=['city', 'date'], keep='first')?

Продолжить с проверкой и прогрессом

Откройте интерактивный раннер с заданиями урока.

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