Дубликаты появляются постоянно: форму отправили дважды, выгрузку склеили из двух пересекающихся файлов, запись внесли повторно. Каждый повтор завышает суммы и счётчики. В журнале возвратов «Кофейной лавки» 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: от каждой группы одинаковых строк остаётся первая.
Сохраните в n_exact число строк таблицы returns, которые полностью повторяют более ранние. В unique_rows
сохраните таблицу без таких повторов.
returns.duplicated().sum().returns.drop_duplicates().Заготовка упражнения обновилась с тех пор, как вы сохранили свой код.
Вернуть заготовку? Ваш код в этом упражнении будет удалён.
n_exact = ...
unique_rows = ...Упражнение пока не решено, поэтому для ячеек ниже выполнено эталонное решение — ваш код не изменён.
n_exact = returns.duplicated().sum()
unique_rows = returns.drop_duplicates()Повторы по ключу
Осталось 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
Шесть возвратов записаны дважды.
Сохраните в doubles все записи таблицы unique_rows, у которых пара order_id и product_id встречается
больше одного раза, — обе записи каждой пары. Отсортируйте doubles по order_id и product_id.
unique_rows.duplicated(subset=["order_id", "product_id"], keep=False).sort_values(["order_id", "product_id"]).Заготовка упражнения обновилась с тех пор, как вы сохранили свой код.
Вернуть заготовку? Ваш код в этом упражнении будет удалён.
doubles = ...Упражнение пока не решено, поэтому для ячеек ниже выполнено эталонное решение — ваш код не изменён.
doubles = unique_rows[unique_rows.duplicated(subset=["order_id", "product_id"], keep=False)]
doubles = doubles.sort_values(["order_id", "product_id"])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())?
[False, True, False, True], сумма 2.Индекс после удаления
Удалённые строки уносят свои метки — в индексе остаются дыры:
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 | привезли не тот товар |
Сохраните в clean таблицу unique_rows, в которой от каждого задвоенного возврата (пара order_id,
product_id) оставлена первая запись, а индекс идёт с нуля подряд. В items_raw сохраните сумму столбца
quantity в исходной таблице returns, в items_clean — в clean.
unique_rows.drop_duplicates(subset=["order_id", "product_id"]).reset_index(drop=True).Заготовка упражнения обновилась с тех пор, как вы сохранили свой код.
Вернуть заготовку? Ваш код в этом упражнении будет удалён.
clean = ...
items_raw = ...
items_clean = ...Упражнение пока не решено, поэтому для ячеек ниже выполнено эталонное решение — ваш код не изменён.
clean = unique_rows.drop_duplicates(subset=["order_id", "product_id"]).reset_index(drop=True)
items_raw = returns["quantity"].sum()
items_clean = clean["quantity"].sum()По сырому журналу возвращено 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
В таблице signups — заявки на рассылку. Приведите столбец email к единому виду (без пробелов по краям,
строчными буквами) и запишите обратно в signups. Затем сохраните в people таблицу без повторов с индексом
с нуля, а в n_people — число людей.
signups["email"] = signups["email"].str.strip().str.lower().signups.drop_duplicates().reset_index(drop=True).Заготовка упражнения обновилась с тех пор, как вы сохранили свой код.
Вернуть заготовку? Ваш код в этом упражнении будет удалён.
# приведите signups["email"] к единому виду
people = ...
n_people = ...Упражнение пока не решено, поэтому для ячеек ниже выполнено эталонное решение — ваш код не изменён.
signups["email"] = signups["email"].str.strip().str.lower()
people = signups.drop_duplicates().reset_index(drop=True)
n_people = len(people)Порядок очистки поэтому всегда такой: сначала типы и текст, потом дубликаты.
Итоги
duplicated()— маска повторов (первое появление не отмечено);keep=Falseотмечает всех.drop_duplicates()удаляет повторы;subset=[...]— сравнивать только эти столбцы;keep— какую запись оставить.- Слишком грубый ключ удаляет настоящие записи; грязный текст прячет дубликаты.
- После удаления строк —
reset_index(drop=True), если нужны метки подряд.