
本文详解如何在 Pandas DataFrame 中,按 article_id 分组并严格保留每个分组中 created_at 时间戳最晚的一条记录,解决日志类数据中“覆盖式操作”导致的冗余问题。
本文详解如何在 pandas dataframe 中,按 `article_id` 分组并严格保留每个分组中 `created_at` 时间戳最晚的一条记录,解决日志类数据中“覆盖式操作”导致的冗余问题。
在处理用户行为日志(如文章审核、状态变更等)时,常见一种「操作覆盖」模式:同一 article_id 可能被同一或不同用户多次修改,仅需保留最后一次有效操作——而非简单按行序取最后一条。用户原始尝试 df.drop_duplicates(subset=['article_id'], keep='last') 失败的根本原因在于:keep='last' 依据的是原始 DataFrame 的物理行序,而非业务逻辑中的时间先后顺序。若数据未按时间排序,该操作将随机保留某次操作,导致结果不可靠(如示例中误删大量早期用户记录,行数异常锐减)。
正确做法是:先按时间戳升序排序,再对 article_id 去重并保留最后一条。这样可确保每个 article_id 对应的记录,一定是其所有历史操作中发生时间最晚的一条。
✅ 标准实现步骤
import pandas as pd
# 1. 确保 created_at 列为 datetime 类型(关键!)
df['created_at'] = pd.to_datetime(df['created_at'])
# 2. 按时间戳升序排序(使最新记录位于每组末尾)
df_sorted = df.sort_values('created_at')
# 3. 基于 article_id 去重,保留每组中排序后位置最靠后的记录(即时间最新者)
df_latest = df_sorted.drop_duplicates(subset='article_id', keep='last')
# 4. (可选)格式化时间输出(如仅保留日期)
df_latest['created_at'] = df_latest['created_at'].dt.strftime('%Y-%m-%d')⚠️ 注意:sort_values() 默认升序(ascending=True),这是必需的——只有升序后,keep='last' 才对应时间最晚的记录。若误用降序,则 keep='last' 将保留最早记录,结果完全相反。
? 验证逻辑合理性(以 article_id=3 为例)
原始数据中 article_id=3 共有 4 条记录,created_at 时间戳依次为:
立即学习“Python免费学习笔记(深入)”;
- 2023-12-05T20:06:31.980Z
- 2023-12-05T20:06:33.730Z
- 2023-12-05T20:06:36.387Z
- 2023-12-05T20:06:56.200Z ← 最新
排序后该组四条记录位于连续位置,drop_duplicates(..., keep='last') 将精准保留第 4 条,即最终生效的操作(value= -1),符合业务预期。
? 进阶建议与注意事项
- 性能优化:对于超大数据集(如原文 17,349 行),建议在 sort_values 后立即 reset_index(drop=True),避免索引碎片化影响后续操作;
- 多字段唯一性判断:若需同时考虑 user_id + article_id 组合的最新操作,可将 subset 改为 ['user_id', 'article_id'];
- 保留原始索引:若需追溯原始日志行号,可在排序前添加 df = df.reset_index().rename(columns={'index': 'original_idx'}),去重后仍可映射回源文件;
- 时间精度校验:确保 created_at 解析无歧义(如 ISO 格式 2023-12-05T20:06:56.200Z 可被 pd.to_datetime 自动识别;若含不规范格式,需显式指定 format 参数或使用 infer_datetime_format=True)。
执行上述流程后,数据量将从 17,349 行收敛至实际独立 article_id 的数量级(约 7,000+),且每条记录均代表对应文章的终局状态,可直接用于统计分析或下游决策系统。










