Аналитика28 августа 2026 г.

Дубли в pandas: как выбрать ключ и сохранить причину удаления строки

Автор
Ненашев А.
На чтение
8 мин
Дата обновления
28.08.2026
Старт:В любой момент
Срок обучения:3 месяца
Погружение в мир AI: наука о данных и машинное обучение
Курс «Погружение в мир AI: наука о данных и машинное обучение» предлагает всестороннее обучение ключевым аспектам науки о данных и машинного обучения. Вы освоите Python и Pandas для обработки данных, регрессию и прогнозирование, классификацию, кластерный анализ, нейронные сети и текстовый анализ. Включены практические занятия по настройке Anaconda, работе с типами и структурами данных, методам и визуализации данных. Также вы научитесь создавать модели регрессии и классификации, проводить кластерный анализ и использовать нейронные сети.
Подробнее
17 994 ₽35 988 ₽
#COURSE##INNER#

Дубли в pandas: как выбрать ключ и сохранить причину удаления строки

Когда в таблице появляются повторы, соблазн понятен: вызвать drop_duplicates() и получить «чистый» DataFrame. Но для рабочих данных это часто слишком грубо. Полное совпадение по всем столбцам встречается реже, чем повтор одного и того же бизнес-события с разными техническими полями: временем загрузки, статусом, источником файла. Если удалить строки без условия, можно потерять нужную версию записи или, наоборот, оставить устаревшую.

Узкий и практический вопрос здесь такой: как выбрать столбцы, по которым запись считается повтором, и как сохранить понятную причину, почему конкретная строка была исключена. Ниже разберем один учебный кейс целиком: от выбора бизнес-ключа до журнала исключенных строк и отдельной очереди на ручную проверку.

Что считать повтором в вашей таблице

В pandas строка считается дублем относительно тех столбцов, которые вы передали в subset. Если subset не указан, метод сравнивает все колонки сразу. Технически это корректно, но аналитически не всегда полезно: два повтора одного счета могут отличаться только полем source_file или временем загрузки, и тогда полное сравнение их не поймает.

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

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

Бизнес-ключ и полное совпадение: на одном примере

Учебный пример. Среда: Python 3.11, pandas, Jupyter или блокнот Anaconda. Представим выгрузку счетов из нескольких файлов. Один и тот же счет мог прийти повторно, а иногда — прийти с обновленным статусом. Для нас бизнес-ключом будут client_id и invoice_no. Полное совпадение по всем столбцам здесь не подходит, потому что технические поля различаются.

import pandas as pd

records = [
    {"row_id": 1, "client_id": "C01", "invoice_no": "A100", "invoice_date": "2024-06-01", "amount": 1200, "status": "new",  "loaded_at": "2024-06-05 09:00", "source_file": "batch_1.csv"},
    {"row_id": 2, "client_id": "C01", "invoice_no": "A100", "invoice_date": "2024-06-01", "amount": 1200, "status": "paid", "loaded_at": "2024-06-05 12:00", "source_file": "batch_2.csv"},
    {"row_id": 3, "client_id": "C01", "invoice_no": "A100", "invoice_date": "2024-06-01", "amount": 1200, "status": "paid", "loaded_at": "2024-06-05 12:00", "source_file": "batch_2_copy.csv"},
    {"row_id": 4, "client_id": "C02", "invoice_no": "B200", "invoice_date": "2024-06-02", "amount": 800,  "status": "new",  "loaded_at": "2024-06-05 09:10", "source_file": "batch_1.csv"},
    {"row_id": 5, "client_id": "C02", "invoice_no": "B200", "invoice_date": "2024-06-02", "amount": 900,  "status": "new",  "loaded_at": "2024-06-05 09:20", "source_file": "batch_2.csv"},
    {"row_id": 6, "client_id": "C03", "invoice_no": "C300", "invoice_date": "2024-06-03", "amount": 500,  "status": "new",  "loaded_at": "2024-06-05 10:00", "source_file": "batch_3.csv"},
    {"row_id": 7, "client_id": "C03", "invoice_no": "C300", "invoice_date": "2024-06-03", "amount": 500,  "status": "new",  "loaded_at": "2024-06-05 10:00", "source_file": "batch_3_copy.csv"},
    {"row_id": 8, "client_id": "C04", "invoice_no": "D400", "invoice_date": "2024-06-04", "amount": 700,  "status": "new",  "loaded_at": "2024-06-05 11:00", "source_file": "batch_4.csv"},
]

df = pd.DataFrame(records)
df["invoice_date"] = pd.to_datetime(df["invoice_date"])
df["loaded_at"] = pd.to_datetime(df["loaded_at"])

key_cols = ["client_id", "invoice_no"]
df["is_key_duplicate"] = df.duplicated(subset=key_cols, keep=False)

Если искать дубли по всем столбцам, группа A100 почти не сократится: там отличается статус и имя файла. Но с точки зрения учета это один и тот же счет. Напротив, у пары B200 совпадает ключ, но отличается сумма. Это уже не тот случай, где строку стоит удалять автоматически. Значит, нам нужен не просто фильтр дублей, а правило разветвления: что можно схлопнуть автоматически, а что нужно отдать на разбор.

Как заранее зафиксировать правило выбора строки

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

Отдельно надо решить, какие поля внутри одного ключа обязаны совпадать. Для счета обычно подозрительно, если внутри одной пары client_id + invoice_no различаются сумма или дата счета. Такие случаи лучше не чистить автоматически. Их правильнее пометить как конфликтные группы.

status_priority = {"paid": 2, "new": 1, "cancelled": 0}
df["status_rank"] = df["status"].map(status_priority).fillna(-1).astype(int)

group_check = (
    df.groupby(key_cols, dropna=False)
      .agg(
          rows_in_key=("row_id", "size"),
          amount_nunique=("amount", "nunique"),
          date_nunique=("invoice_date", "nunique")
      )
      .reset_index()
)

group_check["needs_review"] = (
    (group_check["rows_in_key"] > 1) &
    ((group_check["amount_nunique"] > 1) | (group_check["date_nunique"] > 1))
)

df = df.merge(group_check[key_cols + ["rows_in_key", "needs_review"]], on=key_cols, how="left")

review_df = df[df["needs_review"]].copy()
work_df = df[~df["needs_review"]].copy()

work_df = work_df.sort_values(
    key_cols + ["status_rank", "loaded_at", "row_id"],
    ascending=[True, True, False, False, True]
)
work_df["keep_row"] = ~work_df.duplicated(subset=key_cols, keep="first")
keepers = work_df[work_df["keep_row"]].copy()

В этом учебном наборе правило дает предсказуемый результат. Для A100 остается строка 2: статус paid выше, чем new. Строка 3 выглядит как техническая копия той же предпочтительной версии. Для C300 из двух одинаковых по смыслу строк остается 6-я, потому что она выигрывает тай-брейк по row_id. А пара B200 уходит в ручной разбор: суммы 800 и 900 внутри одного ключа не стоит схлопывать без дополнительного контекста.

Как собрать журнал исключенных строк с причиной

Если сразу вызвать drop_duplicates, вы потеряете объяснение, что именно исчезло. Для контрольной таблицы удобнее сначала выделить предпочтительные строки, а затем собрать все проигравшие записи в отдельный журнал. В нем должны быть как минимум: исходный row_id, сохраненный kept_row_id, бизнес-ключ и причина исключения.

excluded = work_df[(work_df["rows_in_key"] > 1) & ~work_df["keep_row"]].copy()

winner_info = keepers[key_cols + ["row_id", "status_rank", "loaded_at"]].rename(
    columns={
        "row_id": "kept_row_id",
        "status_rank": "kept_status_rank",
        "loaded_at": "kept_loaded_at"
    }
)

excluded = excluded.merge(winner_info, on=key_cols, how="left")

def get_reason(row):
    if row["status_rank"] < row["kept_status_rank"]:
        return "ниже приоритет статуса для того же бизнес-ключа"
    if row["loaded_at"] < row["kept_loaded_at"]:
        return "оставлена более поздняя загрузка"
    return "техническая копия предпочтительной записи"

excluded["duplicate_reason"] = excluded.apply(get_reason, axis=1)
control_log = excluded[["row_id", "kept_row_id", "client_id", "invoice_no", "duplicate_reason"]]

Проверяемый итог учебного кейса такой: из 8 строк автоматически сохраняются 3 строки в keepers — это 2, 6 и 8. В ручной разбор уходят 2 строки — 4 и 5. В журнал исключений попадают 3 строки — 1, 3 и 7. Сумма 3 + 2 + 3 дает исходные 8 строк, значит ни одна запись не потерялась между этапами.

ГруппаЧто произошлоРезультат
C01 + A100Повтор одного счета, статусы и файлы различаютсяОставлена строка 2, строки 1 и 3 записаны в журнал
C02 + B200Внутри ключа различается amountАвтоудаления нет, строки 4 и 5 отправлены на проверку
C03 + C300Техническая копия одной и той же записиОставлена строка 6, строка 7 записана в журнал
C04 + D400Уникальная записьОстается без изменений

Такой журнал полезен не только для отладки. По нему можно объяснить коллегам, почему строка исчезла из витрины, и отдельно посчитать типы проблем: устаревшие версии, технические копии, конфликтные ключи.

Как проверить результат перед публикацией таблицы

После очистки стоит проверить не только количество строк, но и логику решения. Во-первых, каждая исходная строка должна оказаться ровно в одном из трех наборов: keepers, control_log или review_df. Во-вторых, по каждому бизнес-ключу, который не ушел на ручной разбор, должна остаться ровно одна строка. В-третьих, причины исключения должны быть сформулированы так, чтобы их можно было понять без чтения кода.

Есть и практический критерий качества: сможете ли вы через неделю ответить, почему была удалена строка 7, не пересобирая весь расчет заново. Если ответ хранится только в сортировке внутри ноутбука, процесс хрупкий. Если он лежит в контрольной таблице отдельным полем, решение можно проверить и переиспользовать.

  • Не ищите дубли по всем столбцам, пока не назвали бизнес-ключ.
  • Отделяйте авторазрешимые повторы от конфликтных групп, где меняются важные поля.
  • Фиксируйте правило выбора строки заранее: приоритет статуса, время загрузки, тай-брейк.
  • Собирайте журнал исключенных строк вместо молчаливого удаления.
  • Проверяйте, что все исходные строки разложились по итоговым наборам без потерь.

Если хотите потренировать такой подход в учебной среде, в карточке курса «Погружение в мир AI: наука о данных и машинное обучение» на Азбуке курсов и в подтвержденной программе школы есть темы по работе с DataFrame, чтению CSV/XLSX/SQL и подготовке данных в Pandas, включая удаление дубликатов, обработку пропусков и группировку. Для задачи с журналом дублей полезна именно связка этих тем, а не одно действие drop_duplicates().

Курсы

155863
0,0
0 отзывов
Погружение в мир AI: наука о данных и машинное обучение
  • В любой момент
  • 3 месяца
  • самостоятельно
  • диплом
17 994 ₽35 988 ₽
-50%
Перейти на сайт