Условие
В таблице contacts(email, name, source, updated_at) для одного email бывает несколько строк из разных источников. Нужно оставить одну строку на email — самую свежую по updated_at, а при равенстве — из источника crm (приоритет над web).
Решение
Подход
sort_values по нескольким ключам в нужном порядке + drop_duplicates(keep='first').
Реализация
priority = {'crm': 0, 'web': 1, 'mobile': 2, 'csv_import': 3}
contacts['source_rank'] = contacts['source'].map(priority).fillna(99)
dedup = (
contacts
.sort_values(
['email', 'updated_at', 'source_rank'],
ascending=[True, False, True], # email any, свежие сверху, приоритетные сверху
)
.drop_duplicates(subset='email', keep='first')
.drop(columns='source_rank')
)Альтернатива через groupby
dedup = (
contacts
.assign(source_rank=contacts['source'].map(priority).fillna(99))
.sort_values(['updated_at', 'source_rank'], ascending=[False, True])
.groupby('email', as_index=False)
.first()
)Подводные камни
drop_duplicates(keep='first')зависит от порядка строк — безsort_valuesрезультат недетерминирован.- NaN в
updated_atсортируются в конец независимо отascending(опцияna_position='last'). - Если email в разном регистре (
A@x.comvsa@x.com) — это разные строки. Сначалаemail.str.lower().str.strip(). groupby(...).first()берёт первое не-NaN значение по каждой колонке — это часто не то же, что «первая строка целиком». Будьте аккуратны.- Невидимые пробелы в email —
, табы —str.strip()их не убирает. Используйтеstr.replace(r'\s+', '', regex=True).
Эталонный ответ
sort_values([...]).drop_duplicates(subset='email', keep='first') после нормализации регистра и пробелов.