Нашли ошибку?
Скачать чистый
урок 5 из 7≈ 20 минупражнения: 0 из 4

Дубликаты

Найти и удалить повторы: полные и по ключевым столбцам; какую запись оставить; почему слишком грубый ключ удаляет лишнее, а грязный текст прячет дубликаты; reset_index после удаления строк.

Python запустится при первом выполнении

Дубликаты появляются постоянно: форму отправили дважды, выгрузку склеили из двух пересекающихся файлов, запись внесли повторно. Каждый повтор завышает суммы и счётчики. В журнале возвратов «Кофейной лавки» 163 записи — посмотрим, сколько из них настоящих.

[ ]Журнал возвратов
import pandas as pd

returns = pd.read_csv("data/returns_raw.csv")
print(len(returns))
returns.head(4)
Выводсохранённый · нажмите «Выполнить», чтобы получить свой
163
return_id order_id product_id return_date quantity reason
0 R001 10001 P18 17.01.2025 2 брак
1 R002 10010 P16 2025-01-19 1 повреждена упаковка
2 R003 10012 P01 21.01.2025 2 передумал
3 R004 10018 P03 2025-01-15 5 не понравился вкус

Полные повторы

Метод duplicated возвращает маску: True у строки, которая целиком повторяет одну из строк выше. На маленькой таблице это видно целиком:

[ ]Кто уже встречался
visits = pd.DataFrame({
    "client": ["Анна", "Олег", "Анна", "Анна", "Олег"],
    "drink": ["латте", "эспрессо", "латте", "какао", "эспрессо"],
})
visits["repeat"] = visits.duplicated()
visits
Выводсохранённый · нажмите «Выполнить», чтобы получить свой
client drink repeat
0 Анна латте False
1 Олег эспрессо False
2 Анна латте True
3 Анна какао False
4 Олег эспрессо True

Строка 2 повторяет строку 0, строка 4 — строку 1. Строка 3 — не повтор: клиент тот же, но напиток другой. Первое появление повтором не считается — отмечены только вторые и следующие.

Сумма маски — сколько в таблице лишних строк:

[ ]Лишних строк в маленькой таблице
print(visits["repeat"].sum())
Выводсохранённый · нажмите «Выполнить», чтобы получить свой
2

Чтобы увидеть повторы вместе с оригиналами, есть параметр keep=False — «не оставлять без отметки никого». После сортировки пары встают рядом:

[ ]Одинаковые строки
returns[returns.duplicated(keep=False)].sort_values("return_id").head(6)
Выводсохранённый · нажмите «Выполнить», чтобы получить свой
return_id order_id product_id return_date quantity reason
17 R016 10151 P02 2025-02-16 2 брак
45 R016 10151 P02 2025-02-16 2 брак
27 R026 10323 P06 2025-03-23 1 повреждена упаковка
135 R026 10323 P06 2025-03-23 1 повреждена упаковка
13 R035 10442 P03 2025-04-19 1 Передумал
36 R035 10442 P03 2025-04-19 1 Передумал

Удаляет повторы метод drop_duplicates: от каждой группы одинаковых строк остаётся первая.

[ ]Упражнение 1Убрать полные повторы решено

Сохраните в n_exact число строк таблицы returns, которые полностью повторяют более ранние. В unique_rows сохраните таблицу без таких повторов.

n_exact = ...
unique_rows = ...

Повторы по ключу

Осталось 156 записей, и все номера возвратов return_id разные. Но дубликат не обязан совпадать во всём. Один и тот же возврат могли оформить дважды — под двумя номерами. По смыслу возврат — это товар из заказа: пара «заказ, товар». Параметр subset велит сравнивать только указанные столбцы:

[ ]Номера разные, а пары «заказ — товар» повторяются
print(unique_rows["return_id"].nunique(), len(unique_rows))
print(unique_rows.duplicated(subset=["order_id", "product_id"]).sum())
Выводсохранённый · нажмите «Выполнить», чтобы получить свой
156 156
6

Шесть возвратов записаны дважды.

[ ]Упражнение 2Показать задвоенные возвраты решено

Сохраните в doubles все записи таблицы unique_rows, у которых пара order_id и product_id встречается больше одного раза, — обе записи каждой пары. Отсортируйте doubles по order_id и product_id.

doubles = ...

[ ]Один возврат — два номера
doubles.head(4)
Выводсохранённый · нажмите «Выполнить», чтобы получить свой
return_id order_id product_id return_date quantity reason
57 R054 10594 P10 28/05/2025 1 Повреждена упаковка
162 R156 10594 P10 28/05/2025 1 Повреждена упаковка
63 R060 10662 P10 30/05/2025 1 не понравился вкус
159 R153 10662 P10 30/05/2025 1 не понравился вкус
Вывод появится, когда решите упражнение «Показать задвоенные возвраты», — иначе он подскажет ответ. Ячейку можно и выполнить.

Смотреть на найденные дубликаты глазами обязательно: только так видно, действительно ли это одна и та же запись.

Какую запись оставить

У drop_duplicates тот же параметр keep: "first" (по умолчанию) оставляет первую запись из группы, "last" — последнюю, False — удаляет все.

[ ]Оставить первую или последнюю
first = unique_rows.drop_duplicates(subset=["order_id", "product_id"])
last = unique_rows.drop_duplicates(subset=["order_id", "product_id"], keep="last")
print(len(first), len(last))
print(first["return_id"].tail(3).tolist())
print(last["return_id"].tail(3).tolist())
Выводсохранённый · нажмите «Выполнить», чтобы получить свой
150 150
['R148', 'R149', 'R150']
['R154', 'R155', 'R156']

Строк в обоих случаях 150, но записи разные. Какая правильнее, зависит от данных: если поздняя запись — исправление ранней, нужна последняя; если повтор — случайная вторая отправка, первая.

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

[ ]Слишком грубый ключ
len(unique_rows.drop_duplicates(subset=["order_id"]))
Выводсохранённый · нажмите «Выполнить», чтобы получить свой
146
Вопрос

Что выведет print(pd.Series([1, 1, 2, 1]).duplicated().sum())?

Индекс после удаления

Удалённые строки уносят свои метки — в индексе остаются дыры:

[ ]Строк 150, а последняя метка — 156
first.tail(3)
Выводсохранённый · нажмите «Выполнить», чтобы получить свой
return_id order_id product_id return_date quantity reason
154 R148 11563 P06 04.01.2026 2 передумал
155 R149 11566 P09 2026-01-05 2 привезли не тот товар
156 R150 11575 P11 2026-01-15 2 привезли не тот товар

Это не ошибка, но мешает: loc[149] может не найтись, а номер последней строки не равен числу строк. Метод reset_index нумерует строки заново. С параметром drop=True старые метки просто выбрасываются; без него они сохранились бы отдельным столбцом index.

[ ]Метки снова подряд
first.reset_index(drop=True).tail(3)
Выводсохранённый · нажмите «Выполнить», чтобы получить свой
return_id order_id product_id return_date quantity reason
147 R148 11563 P06 04.01.2026 2 передумал
148 R149 11566 P09 2026-01-05 2 привезли не тот товар
149 R150 11575 P11 2026-01-15 2 привезли не тот товар
[ ]Упражнение 3Чистый журнал решено

Сохраните в clean таблицу unique_rows, в которой от каждого задвоенного возврата (пара order_id, product_id) оставлена первая запись, а индекс идёт с нуля подряд. В items_raw сохраните сумму столбца quantity в исходной таблице returns, в items_clean — в clean.

clean = ...
items_raw = ...
items_clean = ...

По сырому журналу возвращено 230 штук товара, по чистому — 214. Тринадцать лишних записей завышали итог на 7 %.

Пояснение с ответом появится, когда решите упражнение «Чистый журнал».

Сначала почистить, потом искать

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

[ ]Пять адресов, ни одного повтора?
signups = pd.DataFrame({
    "email": ["anna@lavka.example", "Anna@lavka.example ", "oleg@lavka.example", "OLEG@LAVKA.EXAMPLE", "maria@lavka.example"],
    "city": ["Казань", "Казань", "Москва", "Москва", "Казань"],
})
print(signups.duplicated().sum())
print(signups["email"].nunique())
Выводсохранённый · нажмите «Выполнить», чтобы получить свой
0
5
[ ]Упражнение 4Кто подписался решено

В таблице signups — заявки на рассылку. Приведите столбец email к единому виду (без пробелов по краям, строчными буквами) и запишите обратно в signups. Затем сохраните в people таблицу без повторов с индексом с нуля, а в n_people — число людей.

# приведите signups["email"] к единому виду
people = ...
n_people = ...

Порядок очистки поэтому всегда такой: сначала типы и текст, потом дубликаты.

Итоги

  • duplicated() — маска повторов (первое появление не отмечено); keep=False отмечает всех.
  • drop_duplicates() удаляет повторы; subset=[...] — сравнивать только эти столбцы; keep — какую запись оставить.
  • Слишком грубый ключ удаляет настоящие записи; грязный текст прячет дубликаты.
  • После удаления строк — reset_index(drop=True), если нужны метки подряд.

Подробнее в справочнике

Нашли ошибку?