ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Python数据质量报告实战:缺失率、重复率、异常值检测

Python数据质量报告实战:缺失率、重复率、异常值检测 1. 为什么数据项目里必须有质量报告这一环做爬虫和数据分析的朋友应该都有过这种经历吭哧吭哧写完爬虫跑了一晚上第二天打开数据库一看——好家伙有的字段空了一大片有的记录重复了好几条个别数值离谱到能穿越物理定律。数据是拿回来了但它到底能不能直接进模型、能不能直接出报表、能不能放心地去算 KPI你有没有一种凭感觉的不确定感。我在做数据采集和清洗的时候最常用的一个自查动作就是把数据的健康状况量化成一张报告重点盯三个指标——缺失率、重复率、异常值 TopN。不夸张地说这一步几乎决定了后续分析结论的可信度。如果数据里埋着 30% 的缺失你算出来的均值、比例、趋势全都可能是假的。当时我把这套检查逻辑写成了一节完整的 Python 爬虫实战课项目名叫质量报告自动生成。它做的事很简单拿到一批采集好的数据之后程序自动扫一遍输出每个字段的缺失情况、整体重复记录占比、以及数值型字段里偏离正常范围最大的前 N 条记录最终汇总成一份可以直接翻看的质量报告。如果你也在爬数据、存数据、用数据这一节内容应该能帮你省掉很多手动排查的时间。适合谁看刚学完 requests、pandas 基础操作、正准备做第一个完整小项目的新手以及已经有爬虫经验、但每次数据落库后都要靠肉眼和 Excel 筛来筛去的老手。零基础不是问题我会把每一步的为什么这么做也讲清楚照着一行一行敲完你手里就有了一套能复用的数据体检工具。2. 整体思路拆解质量报告到底要查什么2.1 三个核心指标的物理意义先别急着写代码得先把质量报告这件事本身想明白。数据质量检查不是拿着放大镜看每一行是否完美而是抓住三类最容易让下游分析翻车的问题。第一个是缺失率。一个字段如果有一大半都是空值它在统计分析里的可信度就很低。比如你爬商品数据价格字段缺失 40%那你算出来的平均价格只代表那 60% 的商品万一缺失的恰好是高价商品你的结论就偏了。所以缺失率不是简单地报个数而是要告诉使用者哪个字段最不靠谱、大概缺了多少、需不需要重新采集或者填充。第二个是重复率。爬虫在翻页、去重、断点续爬的时候特别容易出现重复记录。重复数据会让计数类统计失真你统计一共有多少条商品结果同一件商品因为页面刷新被爬了两遍数字就虚高了。更麻烦的是重复记录如果带着不同的字段值还会造成数据口径混乱。第三个是异常值 TopN。数据里最扎眼的个例不一定都是错误但它们是最值得人工看一眼的。比如年龄字段出现 999、价格字段出现负数、温度字段出现 80 度这些要么是采集时解析错位要么是原始页面本身有噪声。不做清洗直接拿去用一个异常点就能把均值拉偏好几个单位。2.2 为什么用报告而不是报错我在设计这个项目的时候故意把它做成生成报告而不是发现错误就报错终止。原因很现实爬虫采集的数据量大而且来源杂你不能指望数据是完美的。质量报告的价值在于全景呈现、辅助决策它告诉你每个字段的底色再由你决定哪些能忍、哪些必须清。打个生活化的比方体检报告不会在你血压偏高时直接报警让你别活了而是告诉你这个指标在什么范围超了多少建议关注什么。数据质量报告也是这个逻辑它给的是一个客观描述而不是一刀切的对错判断。所以我采用了三件事的设计逐字段统计缺失值和缺失率按严重程度排序。计算整个数据集的行级重复数量与重复率。对数值型字段做 IQR 或固定阈值扫描找出 TopN 的异常样本。这三件事做完一张表、一个 DataFrame、几条打印信息就能把一批数据的基本健康状况交代清楚。2.3 放在爬虫流程的哪个位置新建一个爬虫项目时我习惯把流程拆成四段请求与解析、数据清洗、质量检查、存储落库。质量检查正好卡在清洗之后、落库之前这样能在最早的时间点发现问题避免脏数据进库之后再来回折腾。如果你的爬虫是边爬边存那可以在每次批量写入之前调用一次质量检查函数如果你是全部爬完再统一处理那就把检查放在合并 DataFrame之后。两种方式我都试过前者适合怕入库垃圾太多的场景后者适合一次性拿到全量再统一治理。3. 核心细节拆解缺失率、重复率、异常值各自的坑到了这一节就得咬文嚼字了。这三个指标看起来简单但真要落到代码里有非常多看起来对、实际误导人的写法。3.1 缺失率计算的精度问题计算缺失率常用的是 DataFrame.isnull().sum()然后除以总行数。这一步里最大的坑是分母。假如你有一批数据是增量爬取的某些字段只在部分任务里采集那么总行数到底是整个表的总行数还是本批次新增的行数这两种口径算出来的缺失率差距巨大。我在做这个项目时默认用当前传入 DataFrame 的行数作为分母。这样报告反映的是这批数据自身的完整度不会因为历史数据的稀释作用而变得虚假乐观。另一个细节是空字符串算不算缺失。isnull() 只能识别 NaN 和 None但很多爬虫解析时拿到的其实是空字符串 或者 null 这种字符串。如果只看 isnull()你永远发现不了这种污染。所以在统计之前我会先做一步标准化把空字符串、全空格字符串、以及常见的 null、None、NA 等文本统一替换为 NaN再统计缺失。import pandas as pd import numpy as np # 常见的伪缺失文本 placeholder_values [, , null, None, NA, N/A, nan, NaN] def normalize_missing(df): df df.replace(placeholder_values, np.nan) return df这一条前期做爬虫的时候我吃过亏。辛辛苦苦写了一堆判断最后发现很多缺失是字符串形式的肉眼看着是空的代码却认为它有值。3.2 重复率判断不能只靠完全重复重复率这个词也有两套理解。最粗浅的是整行完全相同这种重复一般出现在跑批任务重复执行、或者翻页过程中把同一页数据抓了两遍的场景。用 df.duplicated() 就能判断。但还有更隐蔽的一种关键字段重复其他字段不同。比如爬商品列表商品的唯一 ID 相同但价格、库存、标题因为页面延迟而不同。这种半重复如果不管后续用 ID 做关联时一样会爆出多对多的问题。所以我在报告里同时输出两个指标整行完全重复的行数及占比。按指定主键字段比如 id、url去重后的重复数及占比。使用主键去重之前要确认主键值得信赖。有些网站页面上没有显式 ID那你可能得用标题 链接拼一个复合主键。保守起见不要拿标题价格这种会变动的组合做主键宁可加一列 hash 指纹。def duplicate_stats(df, primary_keyNone): total len(df) full_dup df.duplicated().sum() info { total_rows: total, full_duplicate_rows: int(full_dup), full_duplicate_rate: round(full_dup / total * 100, 2) if total else 0 } if primary_key: pk_dup df.duplicated(subsetprimary_key).sum() info[primary_key_duplicate_rows] int(pk_dup) info[primary_key_duplicate_rate] round(pk_dup / total * 100, 2) if total else 0 return info注意这里用 subset 参数指定主键列表时请确认 DataFrame 里确实包含这些列否则会抛 KeyError。调用前做一个列名交集校验会更保险。3.3 异常值 TopN 的两种判定策略异常值的定义为偏离正常分布太多但什么是太多没有绝对标准。我在这项目里内置了两种策略并且允许调用者传入自定义阈值函数。第一种是固定阈值法。适用于有明确业务边界的字段比如年龄应该在 0~120价格应该大于等于 0折扣率应该在 0~1 之间。超过边界的直接视为异常。这种方式简单粗暴但对业务理解要求高。第二种是IQR四分位距法。它不依赖业务知识纯从数据分布推断正常范围把数据按大小排序取 25% 分位数为 Q175% 分位数为 Q3IQR Q3 - Q1。一般认为低于 Q1 - 1.5×IQR 或高于 Q3 1.5×IQR 的为异常。然后从异常集合里挑出偏离程度最大的 N 条作为 TopN。这里有个细节IQR 法在数据本身比较倾斜时可能把大量正常值误判为异常。比如销量数据呈现长尾分布头部几个爆款很容易被标成异常但它们恰恰是最重要的数据。所以我建议固定阈值优先。没有业务阈值时再退到 IQR。最终结果只作为值得人工复核的清单而不是必须删除的黑名单。def detect_outliers_topn(series, top_n5, methodiqr, lowerNone, upperNone): if method fixed: mask (series lower) | (series upper) else: q1 series.quantile(0.25) q3 series.quantile(0.75) iqr q3 - q1 lower_bound q1 - 1.5 * iqr upper_bound q3 1.5 * iqr mask (series lower_bound) | (series upper_bound) outliers series[mask] if method fixed: distance (outliers - lower).abs().combine(outliers - upper, lambda a, b: max(abs(a) if pd.notna(a) else 0, abs(b) if pd.notna(b) else 0)) else: distance (outliers - series.median()).abs() return outliers.sort_values(ascendingFalse).head(top_n)注意示例里的 distance 计算故意写得比较抽象实际使用时建议直接按业务定义偏离距离。我的习惯是用与中位数的绝对差因为中位数比均值更抗干扰不会因为异常值本身太大把排序带偏。4. 实操过程从原始数据到完整报告4.1 准备一份待体检的数据集先准备好要检查的数据。这里为了演示我模拟了一批电商商品信息包含商品 ID、商品名称、价格、销量、评分、库存等字段。你也可以替换成自己爬虫采集的真实数据。import pandas as pd import numpy as np data { id: [1001, 1002, 1003, 1004, 1005, 1001, 1006, 1007, 1008, 1009], name: [无线鼠标, 机械键盘, 显示器, USB扩展坞, 笔记本支架, 无线鼠标, 摄像头, 耳机, 路由器, 硬盘盒], price: [99, 299, 1299, 79, 149, 99, -5, 599, 399, 899], # -5 是异常值 sales: [120, 80, 15, 200, 90, 120, 300, 60, 45, None], # None 是缺失 rating: [4.5, 4.7, 4.2, 4.8, 4.6, 4.5, 3.9, 4.1, 4.9, None], # None 是缺失 stock: [500, 200, 50, 800, 300, 500, 1000, 60, 400, 0] # 0 可能是异常 } df pd.DataFrame(data) df.loc[5, name] 无线鼠标 # 制造一行完全重复这里我故意埋了雷id1001 出现两次、price 出现负数、sales 和 rating 有缺失、stock 出现 0。用这些数据跑一遍报告正好可以把三种问题全部暴露出来。4.2 缺失率扫描代码下面这段代码负责逐列统计缺失情况。先说清楚它做了什么第 1 步把伪缺失值替换成 NaN。第 2 步用 isnull() 统计每列 NaN 数量。第 3 步除以总行数得到缺失率。第 4 步按缺失率降序排列让最严重的字段排在前面。def missing_report(df): df normalize_missing(df) total len(df) missing_count df.isnull().sum() missing_rate (missing_count / total * 100).round(2) report pd.DataFrame({ 缺失数量: missing_count, 缺失率(%): missing_rate }).sort_values(by缺失率(%), ascendingFalse) return report执行后我的模拟数据会让sales和rating各出现 10% 的缺失其他字段 0%。看起来比例不高但如果真实场景下这个字段是核心指标10% 依然值得警惕尤其是做时间序列分析时缺失点刚好落在关键节点上结论可以完全不一样。4.3 打上质量标签并汇总输出我习惯在生成报告时同时给每条记录打一个质量状态标签正常、重复、异常、缺失严重。这样使用者除了看汇总统计还能快速定位到具体行。def add_quality_flags(df, primary_keyid): df normalize_missing(df.copy()) df[质量标签] 正常 df.loc[df.duplicated(keepFalse), 质量标签] 重复 if primary_key: df.loc[df.duplicated(subsetprimary_key, keepFalse), 质量标签] 关键字段重复 df.loc[df[price] 0, 质量标签] 异常值 df.loc[df[sales].isnull(), 质量标签] 关键字段缺失 return df最后把三块结果缺失报告、重复统计、异常 TopN合并到一个字典里统一打印summary { 缺失报告: missing_report(df), 重复统计: duplicate_stats(df, primary_key[id]), 异常值TopN: detect_outliers_topn(df[price], top_n5, methodfixed, lower0, upper10000) } print(重复统计:, summary[重复统计]) print(缺失报告:) print(summary[缺失报告]) print(异常值TopN:) print(summary[异常值TopN])跑完之后你会得到一个非常直观的结果缺失哪些字段一目了然重复行数清清楚楚异常值也会被单独拎出来展示。4.4 把报告写成本地文件光在控制台打印还不够报告要能沉淀。我在项目里通常会把结果导成一个 CSV 文件再配一段短文本摘要方便直接贴进周报。with pd.ExcelWriter(数据质量报告.xlsx) as writer: summary[缺失报告].to_excel(writer, sheet_name缺失率) pd.DataFrame([summary[重复统计]]).to_excel(writer, sheet_name重复率) summary[异常值TopN].to_excel(writer, sheet_name异常值)这里需要先确认环境里装了 openpyxl不然 ExcelWriter 会报错。如果你不想依赖额外库改成 to_csv 是最省事的。5. 常见问题与排查技巧实录下面这些坑都是我在实际写数据质量报告时踩过的列出来给大家避雷。5.1 明明有空值count() 却不为 0count() 计算的是非空值数量所以有缺失时 count() 小于 len()。但如果你先做了 replace 把空字符串替换为 NaN这一步没问题如果不做替换空字符串会被 count() 算作有效值缺失率就会被低估。排查方法打印 df.dtypes看字段类型是否为 object。如果是 object大概率藏着字符串形态的缺失。5.2 duplicated() 把错行标成重复duplicated() 默认 keepfirst即在重复组里保留第一行其余标 True。如果你希望所有重复行都被标出来请用 keepFalse。但要注意当你有多个主键时subset 列表里的顺序会影响判断吗不会pandas 是按整组值判断的顺序只影响内部排序不影响结果。5.3 IQR 对倾斜数据过于敏感业务数据十有八九是倾斜的。如果直接用 IQR很多正常的高销量商品会被标成异常。我的解决办法是先用分箱看分布或者对数据做 log1p 变换后再用 IQR识别到的异常会更贴近业务直觉。还有一种思路是用中位数绝对偏差 MAD它对异常值的抵抗力比 IQR 更强。5.4 报告时间太长如果你一次性给几十万行数据逐字段做统计速度可能让人着急。两个优化策略只统计需要的列不要对全表所有列无脑扫描。如果只是快速巡检用 df.sample(5000) 抽一个子集去跑效果基本一致但速度能差几倍。在我自己的经验里质量报告不该做成全量计算而应该是快速扫描 重点复核。全量太重抽样反而能更快定位问题。5.5 TopN 不一定是需要删除的记录异常值 TopN 的定位是值得关注不是必须清洗。我在实际项目里见过价格 -5 这种明显解析错位的数据也见过秒杀价 0.01 元这种真实业务。所以报告生成后一定要让人看一眼别让自动化直接删除。5.6 不同类型字段的异常定义完全不同数值型字段适合阈值法和 IQR 法但字符串字段的异常通常指的是格式不符合预期、枚举值出现未知类别、长度异常。这个项目目前专注数值型异常检测如果你想处理文本类字段可以加一层合法取值集合校验不在集合里的值就会被标记。6. 把质量报告模块化融入你的日常数据流程到这里核心功能已经全部实现。最后分享几个我实际使用中的扩展方向你可以按需选用。6.1 做一个可复用的质量检查类现在这些代码都是函数但如果你多个项目都要用建议封装成一个类。初始化时传入 DataFrame 和配置哪些列是主键、哪些列需要检查异常然后调用一个 run() 方法返回所有报告。这样你的爬虫代码里只需要三行就能完成质量检查。6.2 和爬虫采集日志联动我给爬虫加过一条规则每次采集完成后把质量报告摘要自动追加到日志文件里比如本批次新增 5000 条重复率 2%price 缺失率 1.5%。这样一来哪怕报告没有及时打开看日志里也有据可查出了问题能立刻回溯是哪一个批次引入的。6.3 用报告指导爬虫策略调整质量报告不只是防御工具还能倒推爬虫策略。比如你发现某个字段缺失率特别高往往是解析规则没覆盖到某些页面结构重复率突然升高往往是翻页逻辑在边界条件上出了 bug。这些都是我在项目中真实遇到过的拿着报告回头改爬虫比瞎猜高效得多。把这三个指标管好了你的数据才算真正能用。下一节我会继续往深走讲怎么结合抓取频率和数据更新时间做更细维度的时效性监控到时候就能把单批数据的质量检查和长期数据漂移一起管起来。
返回列表