ARTICLE DETAIL

资讯详情

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

爬虫数据质量三件套:缺失率、重复率与异常值检测实战

爬虫数据质量三件套:缺失率、重复率与异常值检测实战 上一节我们把网页数据解析出来了但解析出来不等于能用。我印象特别深早期带项目的时候有位同学把商品页爬了两千多条兴冲冲拿去做分析结果发现“平均价格”比市场价高出一大截。查了半天才反应过来是爬虫在不同页面把“促销价”和“划线价”给混着抓了——这种问题靠肉眼根本扫不出来。所以今天要聊的是数据质量三件套缺失率、重复率、异常值。很多零基础教程喜欢把这几个概念放到分析阶段再讲我反而建议在爬虫阶段就开始做越早做后面越省力。1. 为什么爬下来的数据必须先做“体检”1.1 爬虫数据天生就是“脏”的这不是你的错先说个让人安心的事实爬虫数据不干净很多时候不是代码写得不好而是网页本身就“不配合”。我举个例子你爬一个商品评论页有的用户没填评分那个评分字段就空着有的商品在列表页显示价格但在详情页价格区块是动态加载的你没等到接口返回就抓了拿到一个空字符串还有的页面在不同地区返回的字段名都不一样比如“手机号”有的字段叫mobile有的叫phone。这些情况叠加起来你最终拼出的DataFrame里必然有缺失值、重复行、异常数值。新手最容易犯的错是拿到数据后直接df.to_csv()存下来然后就开始df.mean()、df.groupby()做分析。你分析的是两千条但里面可能有三百条重复、四百条缺了关键字段、五十条数值是乱码。这种数据算出来的任何指标都是在“垃圾进垃圾出”。所以爬到数据之后的第一件事不是存盘而是先给数据做一次“体检”。体检要查的就是三项缺失率、重复率、异常值。这三项过关了你的数据才敢拿去画图、建模、做报告。1.2 数据质量检查到底在查什么简单拆解一下这三项的含义缺失率某个字段列中有多少比例的值是空的。空包括None、NaN、空字符串甚至包括看起来像没填的N/A。缺失率直接影响你能不能用这一列做分析。重复率整个数据集里有多少行是完全一样或者关键字段完全一样的。重复数据会让统计结果“虚胖”比如计数翻倍、平均值被错误加权。异常值偏离正常范围太远的数值。比如商品价格出现-1年龄出现999销售额出现1e10。异常值可能是爬虫解析错误也可能是网页本身有脏数据它们会严重干扰均值、方差等统计量。那为什么把这三项放在“解析与清洗”这一章里因为爬虫的终点不是“拿到数据”而是“拿到能用的数据”。解析只是把网页结构拆开清洗才是让数据真正可用的开始而清洗的第一步就是量化“脏的程度”。只有知道了缺失率多高、哪里重复、哪些值异常你才能决定下一步怎么处理。2. 缺失率第一个要查的指标2.1 怎么看缺失率而不是靠“感觉”很多新手喜欢用df.info()来看有没有空值这个方法本身没问题但df.info()只告诉你每列有多少个非空值它不会告诉你“缺失率到底有多严重”。比如总共爬了5000条某一列有200个空值看起来不多对吧但如果你只关心其中一条子集数据可能影响就大了。所以第一步我建议先算一个缺失率报告。用 pandas 实现非常简洁import pandas as pd def missing_report(df): report pd.DataFrame({ 列名: df.columns, 缺失数量: df.isnull().sum().values, 缺失率: (df.isnull().sum() / len(df) * 100).values }) report[缺失率] report[缺失率].round(2).astype(str) % return report[report[缺失数量] 0].sort_values(缺失数量, ascendingFalse) # 假设 df 是你爬完拼好的 DataFrame print(missing_report(df))这里核心就是df.isnull().sum()它统计每一列有多少个空值。注意isnull()把NaN和None都识别为缺失但空字符串它不会识别。所以我会再加一步统一处理# 把空字符串、N/A、null 等统一转为 NaN df df.replace({: None, N/A: None, null: None, None: None})这一步很重要因为网页解析时拿到的缺失值很多时候是而不是NaN。如果你不替换后面dropna()根本不会生效因为在 pandas 眼里不是缺失值只是一个普通字符串。2.2 缺失率多高才算高高了怎么处理这个没有绝对标准我个人的参考线是这样的缺失率在5% 以下基本不影响大局可以直接忽略或者简单填充即可。缺失率在5%~20%需要处理。如果这一列对后续分析很关键考虑填充如果不关键也可以直接丢弃。缺失率在20% 以上这列数据基本没法用了。如果是个非核心字段直接删列如果是核心字段你需要回到爬虫阶段看看是不是解析规则漏掉了某些页面结构。处理方式就三种我按优先级排直接删除数据量足够大缺失比例不高删掉缺失行最省事。填充数值型数据可以用均值、中位数填充分类型数据可以用众数填充时间序列可以用前向填充。作为独立信息保留有些场景下缺失本身有业务含义。比如用户没填年龄那“年龄缺失”本身就是一个特征可以单独建一列age_missing存 0/1。在爬虫场景里我特别想提醒一点如果你发现某一个字段缺失率异常高比如超过50%不要闷头去填先回头看一眼你的解析代码。大概率是你的xpath或CSS选择器在部分页面没选中元素。我当时爬详情页时有一个字段在列表页和详情页之间字段名不一致导致详情页全部抓空。后来用了一个容错逻辑先找detail_price找不到再找list_price缺失率才降下来。提示缺失率是“指示器”而不只是“麻烦”。它告诉你网站结构可能比你想象中更不统一。看见缺失率先想“为什么缺”再想“怎么补”。3. 重复率数据里有多少“水分”3.1 重复率的计算和它背后的坑重复率是个很有意思的指标特别是想到大家写论文时被“查重率”支配的恐惧数据里也有“查重”。爬虫数据里的重复通常来自几个原因网站列表页和详情页内容重叠、爬虫重复请求同一URL、分页逻辑写错导致同一页被抓多次。计算重复率一行代码就够了# 全行完全重复 dup_rate df.duplicated().sum() / len(df) print(f完全重复率: {dup_rate:.2%}) # 按关键字段重复比如商品ID、订单号、链接URL dup_rate_key df.duplicated(subset[product_id]).sum() / len(df) print(f按商品ID重复率: {dup_rate_key:.2%})很多教程只教你全行去重但实际爬虫场景里全行重复很少见部分字段重复才常见。比如你爬同一件商品的在架信息价格可能每次不同今天99明天109但商品ID是完全一样的。这种情况下全行duplicated()返回False但subset[product_id]就能筛出来。我踩过一个很典型的坑爬某电商平台评论时同一个用户对同一商品只能评论一次但爬虫在翻页过程中因为页码错乱把同一页重复抓了3次。评论内容是一模一样的但评论时间差了几秒所以全行看起来“不完全重复”。后来我用df.drop_duplicates(subset[user_id, product_id, comment_content])才把真正的重复抓出来。3.2 去重实操哪些列可以“安全”去重去重的代码大家都知道是drop_duplicates()但让我认真提醒三个细节keep参数默认是first也就是保留第一次出现的行删掉后续重复的。如果你希望保留最后一次的数据比如价格最新要写成keeplast。inplace参数很多人容易漏。df.drop_duplicates()不会原地修改你必须赋值给新变量或者加inplaceTrue。去重的“粒度”要想清楚。是按整行去重还是按几个核心字段去重这取决于业务。比如爬招聘信息同一公司和同一职位名称重复出现两次可能是两个不同岗位但如果你按“公司名职位名”去重就会误删。我个人写爬虫脚本时的习惯是这样的每次翻页前先记录一下当前页第一条数据的唯一标识比如商品ID下一页开头检查一下如果跟上一页的重复说明翻页逻辑有bug立刻停止。这样可以在源头控制重复而不是一直抓到几千条再去重。seen_ids set() for page in range(1, max_page 1): items fetch_page(page) for item in items: pid item[product_id] if pid in seen_ids: print(f检测到重复ID: {pid}, 第{page}页可能重复抓取) continue seen_ids.add(pid) data_list.append(item)这种方式比你抓到两万条之后再用drop_duplicates()更高效也更容易发现爬虫逻辑本身的问题。4. 异常值藏在数据里的小偷4.1 异常值是怎么混进来的异常值在爬虫数据里的来源主要有三种解析错误正则表达式或xpath选中了错误的节点。比如本应提取价格却提取到了“已售罄”这种文字转成数值时变成乱码。网站自身的脏数据有些页面里的商品划线价是999999用来凸显“打折力度大”这就成了异常值。单位不一致同一字段下有的值是“元”有的是“千元”有的是字符串拼接的¥299.00你转float时没处理好。判断异常值的核心思路是看“这个值是否超出了合理的业务范围”。比如你爬房价数据正常单价范围可能在8000到50000之间突然冒出一个9999999这肯定是异常。但具体怎么科学地找出这些值不能只靠肉眼看df.describe()。下面说三种我常用的方法。4.2 三种最实用的异常值检测方法方法一描述统计法适合快速排查print(df.describe())describe()会给出每列数值的 count、mean、std、min、25%、50%、75%、max。你就看两件事min 和 max 是否离谱mean 是否被某个极值拉偏。比如max9999999而75%只有200那基本可以断定 max 是异常值。方法二3σ 原则适合近似正态分布的数据如果你确认某个字段的数据大致符合正态分布可以用“均值 ± 3倍标准差”作为正常范围超出就视为异常。import numpy as np def find_outliers_3sigma(series): mean series.mean() std series.std() lower mean - 3 * std upper mean 3 * std mask (series lower) | (series upper) return mask outlier_mask find_outliers_3sigma(df[price]) print(f异常值数量: {outlier_mask.sum()})方法三IQR 四分位距法更稳健适合偏态分布但现实里爬虫数据大多不是正态分布价格、销量这种数据往往右偏严重3σ 会把很多正常的高值误判为异常。这时用 IQR 法更稳。所谓 IQR 就是75%分位数 - 25%分位数正常范围是[Q1 - 1.5*IQR, Q3 1.5*IQR]。这个“1.5”是统计学里箱线图默认的系数适合大多数场景。def find_outliers_iqr(series): q1 series.quantile(0.25) q3 series.quantile(0.75) iqr q3 - q1 lower q1 - 1.5 * iqr upper q3 1.5 * iqr mask (series lower) | (series upper) return mask outlier_mask find_outliers_iqr(df[price]) print(fIQR法异常值数量: {outlier_mask.sum()})我个人的实操经验是能用 IQR 就尽量用 IQR它对少量极端值不敏感比 3σ 更稳。当然你确定数据是正态分布的时候3σ 也能用两者可以都跑一下对比。4.3 异常值处理先理解业务再动手删这是我特别想强调的一点。异常值不是“坏值”的代名词先问一句“这个异常是真的异常还是业务本身的特殊情况”。举个例子你爬王者荣耀的战绩数据某位玩家一局打出50杀0死在普通玩家眼里像异常值但如果是职业选手打低端局这是完全真实的战绩。你如果机械地用 IQR 一刀切删掉反而删掉了真实的极端个体。所以异常值的处理策略我按优先级排序核实来源去网页上看一眼这个值是否真实存在。如果网页上就这么显示的那不是爬虫的问题是业务数据本身如此不要轻易删。视为缺失值如果这个值是解析乱码比如字符串混入了数字我会把它先转为 NaN然后按缺失值处理。截尾处理比如价格字段最大值 99% 分位数是 5000超出 10000 的都替换成 5000这种叫“截尾”在抗噪声建模时常用。直接删除只有当你确认这个异常值是纯错误数据且量很少比如千分之一才直接删。注意处理异常值前建议先把异常行单独存成一个outliers.csv留个底。后面万一分析结果不对劲你还能翻旧账排查不至于删完就无迹可寻。5. 完整实操把三项检查串成一个脚本5.1 真实的爬虫后处理场景前面说了那么多概念这里我放一个可以直接“抄作业”的完整示例。假设我们用爬虫抓了一个电商网站的手机商品列表字段包括title标题、price价格、sales月销量、shop_name店铺名、rating评分。数据已经解析成 DataFrame 了现在要做质量体检。import pandas as pd import numpy as np # 模拟一份脏数据 data { title: [手机A, 手机B, 手机C, 手机A, 手机D, None, 手机E, 手机F], price: [1999, 2999, 9999, 1999, 3500, 4999, -5, 1899], sales: [1200, 800, 30000, 1200, 500, None, 900, 100], shop_name: [旗舰店, 专卖店, 旗舰店, 旗舰店, 专卖店, 旗舰店, None, 专卖店], rating: [4.8, 4.5, 2.0, 4.8, 4.2, 5.0, 4.9, 4.3] } df pd.DataFrame(data)这份数据里铺了三个坑第0行和第3行title/price/sales/shop_name/rating完全一样是重复数据。第5行title/sales/shop_name有缺失。第2行价格9999、第6行价格-5是异常值需要判断。5.2 我写的简易“质量报告”函数我会把前面讲到的检查集中到一个函数里每次爬到数据直接跑一遍。def quality_report(df): print( * 50) print(一、缺失率检查) missing df.isnull().sum() missing_rate missing / len(df) for col in df.columns: if missing_rate[col] 0: print(f字段 [{col}] 缺失 {missing[col]} 条缺失率 {missing_rate[col]:.2%}) print( * 50) print(二、重复率检查) full_dup df.duplicated().sum() print(f全行重复: {full_dup} 条重复率 {full_dup / len(df):.2%}) print( * 50) print(三、异常值检查IQR法) for col in [price, sales, rating]: if df[col].dtype in [int64, float64]: q1 df[col].quantile(0.25) q3 df[col].quantile(0.75) iqr q3 - q1 lower q1 - 1.5 * iqr upper q3 1.5 * iqr outliers df[(df[col] lower) | (df[col] upper)] if len(outliers) 0: print(f字段 [{col}] 异常值 {len(outliers)} 条正常范围参考 ({lower:.2f}, {upper:.2f})) quality_report(df)运行结果大概是一、缺失率检查 字段 [title] 缺失 1 条缺失率 12.50% 字段 [sales] 缺失 1 条缺失率 12.50% 字段 [shop_name] 缺失 2 条缺失率 25.00% 二、重复率检查 全行重复: 1 条重复率 12.50% 三、异常值检查IQR法 字段 [price] 异常值 2 条正常范围参考 (-2000.00, 9000.00) 字段 [sales] 异常值 1 条正常范围参考 (-4500.00, 8500.00)注意这里price的 IQR 正常范围下限出现负数因为这份数据本身分布比较特殊。这也说明一个点看正常范围时要结合业务实际价格正常范围不能是负数所以销售价低于0的-5肯定有问题但9999在 IQR 范围内它没有超出报警线。对9999这种值你反而要去业务层面判断一个手机卖 9999可能是高端旗舰机也可能就是划线价的脏数据只靠统计学方法不一定逮得住。5.3 综合处理流程的顺序建议我建议你在爬虫脚本里按下面的顺序走完清洗流程第一步统一空值格式把空字符串等转成 NaN。第二步算缺失率决定哪些列要丢、哪些列要填。第三步按关键字段去重留存一个去重后的副本。第四步跑异常值检测把异常行先另存不要急着删。第五步再次跑一次quality_report()确认三项指标都降到了可接受范围再存最终结果。这样走完你最终存下来的df_clean.csv才是敢放心拿去做分析的数据。我之前接过一个爬小说的项目解析出来的章节内容里夹杂着大量文案广告靠的就是这套“先体检再清洗”的流程把几百个异常行揪出来过滤掉了否则导出的电子书会惨不忍睹。6. 新手最容易踩的坑常见问题与排查技巧6.1 常见问题速查表现象可能原因排查方向isnull().sum()显示 0但明明有空值空值是空字符串或null文本先执行df.replace({: None, null: None})重复率很高但drop_duplicates()没用重复行在某些字段有细微差异如时间戳、空格先用subset指定核心字段必要时先strip()去空格drop_duplicates()删完行数没变忘记赋值或inplace没生效检查是否写成df df.drop_duplicates()price 列里有-5这种异常值但 IQR 没检出数据整体偏态且样本量太小补充业务规则判断比如price 0直接视为异常缺失字段集中在某几页的数据这些页面结构不同解析规则没覆盖回炉检查 xpath打印那几页的HTML结构去重后数据量少得离谱subset选错了字段把不唯一的字段当唯一键确认唯一键比如商品ID才对标题不完全唯一这个速查表是我自己在项目里反复遇到过的不是抽象的理论总结。新手看到第3条可能会觉得离谱但drop_duplicates()忘记赋值真的是最高频错误之一因为 pandas 默认返回新对象不是原地修改。6.2 误区提醒数据清洗不是“能跑就行”我见过不少学习者跑完drop_duplicates()就觉得自己清洗完了。这里我想认真提醒三点清洗要有记录哪怕只是加几行注释也建议把“删了多少行、为什么删、判断标准是什么”写下来。不然一周后你再看这份清洗代码根本想不起当时的判断逻辑。不是所有重复都该删爬招聘网站时同一家公司发布两个相似职位职位名称一样但招聘编号不同这种情况重复吗从业务角度不算。所以去重之前先确认唯一键。异常值不能闭眼删我个人强烈建议把异常值单独存一个文件分析的时候如果有需要再回去翻。删掉的异常值永远找不回来但留着它们又会影响统计结果折中方案就是“隔离”而不是“销毁”。还有一个经验数据质量检查不是一锤子买卖。你爬第一遍和第二遍网页结构可能变了数据质量可能完全不一样。所以我现在的习惯是每次爬完批量任务都会把quality_report()的输出存成一个文本日志放在数据文件夹里。下次重新爬的时候可以先看一眼上次的日志对比这次的质量有没有异常波动如果缺失率突然暴涨大概率是网站改版了。说到网站改版我想起之前爬某网站时遇到过一次前一天缺失率还在3%以下第二天直接飙到40%。我当时第一反应是网络问题但其实是对面的 HTML 结构调整了原来的选择器失效了。还好质量报告跑得勤第一时间发现赶紧去检查解析规则没有把错误数据积压到分析阶段算是用“体检”救回了整个项目。之后你可以顺手把quality_report()这个函数保存成一个独立的data_quality.py文件以后每个爬虫脚本都from data_quality import quality_report调一下。这样既不用反复复制粘贴代码也能保证每个项目的清洗标准一致。别小看这个习惯我后期做爬虫项目时数据清洗的时间占比大约在40%其中质量检查又占了清洗的一半。前期养成这习惯你后面做数据分析和可视化会非常省心至少不会像当初那位同学一样拿着有问题的数据算了半天均值最后才发现结论全偏了。
返回列表