上周三下午我正端着咖啡刷着技术论坛运营组的小姐姐突然在钉钉上弹我“大神数据我们已经帮你清洗过了直接跑模型就行急今天要结果。” 附带一个 CSV 文件名字叫final_clean_v2_最终版.csv。我放下咖啡满怀期待地pd.read_csv然后df.head()。第一行user_age列赫然写着 “-3”第二行是 “二百”第三行是NULL字符串 —— 注意是字符串 “NULL”不是 NaN。register_date那列更离谱2023-02-30明天还有2020/13/01。我当时一口咖啡直接喷在了屏幕上擦的时候就在想你们管这玩意儿叫清洗过这简直是把拖把在脏水里涮了一下就拿来擦脸。干数据这行久了你就明白一个铁律永远不要相信任何人交给你的数据包括你自己上个月存的。数据清洗这个词被说得太轻巧好像真是把白菜上的泥冲掉那么简单。实际上每一次“清洗”都像是在拆盲盒你永远不知道下一个坑是业务逻辑的扭曲、系统迁移的伤痕还是纯粹的人类手滑。今天这篇我就把那些年踩过的脏数据坑揉碎了讲咱们不列什么“清洗三步法”就聊聊那些让你血压飙升的瞬间和怎么擦干净屁股。一、缺失值从来不是空着的那么简单新人最爱问的一句话缺失值怎么处理教科书上写得明明白白删掉、均值/中位数填充、模型预测填充。实际情况呢你先得搞清楚它为什么缺失。我做过一个线下门店的会员分析项目有个字段叫“婚姻状况”40% 是空值。运营说这些肯定都是单身因为已婚的会填配偶信息。结果我查了一下原始录入日志发现门店的平板系统有个 bug当屏幕键盘弹出来的时候“已婚”选项刚好被挡住收银员懒得滑屏幕直接点了下一步系统就默认跳过了。这 40% 的缺失根本不是单身贵族而是被 UI 坑了的已婚人士。如果随手用众数填“未婚”后面所有家庭型消费的关联挖掘全都会歪掉。所以处理缺失值的第一原则是找到缺失的元凶而不是着急补窟窿。怎么找问业务、看日志、交叉验证。有一次我甚至拉了用户绑定的第三方账号信息发现有些用户手机号为空但在微信 OpenID 里绑着手机只是接口没同步过来这种缺失补起来就有底气。实在查不出来我再教你一个用惯了的粗暴分层法把缺失本身就当成一个特征。比如在预测用户流失时“最近一次登录设备” 缺失往往代表该用户长期没登录或者是远古注册用户根本没激活这个缺失本身的信息量就很大。我会单独建一列is_device_missing然后把原字段用中位数填上放进模型里跑特征重要性往往比你想象的高。再不行你就分桶把缺失作为一个单独类别扔给 CatBoost 这种天然支持缺失值的模型省心。千万别上来就df.dropna()。我曾经亲眼见过一个实习生把包含 30% 缺失值的关键字段直接删掉样本量从 50 万变成 3 万后来我问他为啥他说“网上说缺失多了就得删。” 网上还说一天八杯水呢你喝一个我看看。二、异常值不是数学定义的是业务定义的3σ 原则、箱线图 1.5 倍 IQR这些统计学工具是死的。你拿它去套业务数据能把自己套成傻子。讲个真实案例有次做二手车价格预测车辆行驶里程mileage有不少超过 100 万公里的。用箱线图一看远超上须实习生二话不说当异常值全砍了。我回来检查差点没背过气去——那些全是跑了八九十万公里的营运车辆有几辆出租车改的二手网约车它们的价格逻辑跟普通家用车完全不同是市场上真实存在的交易砍掉它们就等于把整个商用二手车市场给剔除了。最后我们把这些“异常”样本单独建模价格预估准确率直接提了 8 个点。所以异常值不是毒药是情报。处理之前先灵魂拷问这个异常在业务上有可能发生吗如果可能它可能就是你的金矿比如金融风控里的欺诈交易恰恰长着一副异常的面孔如果不可能比如年龄字段冒出来个 245 岁那才是真正的脏数据。我还遇到过用户年龄全是 2023 年的数据集一查发现录入页面年月日三个下拉框默认值是当前年份用户懒得改直接点确认所有懒汉都变成了 2023 年出生。这种情况怎么洗我当时的办法是取该用户第一次消费记录的时间用消费时的年龄倒推出生年虽然没法精确到日但至少把婴儿变成了成年人不至于在亲子类目推荐里给一个 30 岁壮汉推奶粉。说个私藏的土办法对于连续变量我会先画一张足够长的直方图不做任何过滤就盯着尾巴看。有一次在用户注册时长分布里看到尾巴上有几个十亿秒的值算了一下大概是 30 多年而 App 才上线 5 年这百分百是时间戳取错了字段可能是把毫秒当秒存了。这种一眼假的东西写个简单的逻辑df.loc[df[reg_seconds] 5*365*24*3600, reg_seconds] np.nan直接干掉不用纠结。三、重复数据麻烦制造机重复数据里最恶心的一类是“不完全重复”你以为用drop_duplicates就能搞定太年轻。电商订单数据里同一个订单号可能因为用户分两次支付出现了两行一行支付 30一行支付 70总额 100但订单号是同一个。业务上这是同一个订单数据上却是两条记录你直接去重会丢金额不去重会重复计算订单量。这种得先聚合用groupby把金额 sum 起来状态取最新的时间取最早的。还有一种幽灵重复CRM 系统里同一个用户有两个 ID因为人家先在小程序注册又在 App 用手机号注册系统没做打通。他的行为数据散落在两个 ID 下面你分析用户画像时要是没合并就会得出一个分裂的结论这个用户既爱买高端护肤品又狂囤尿不湿以为是个精致奶爸其实人家是个宝妈只不过两个号分别记录了她在不同阶段的购物偏好。处理这种问题的唯一办法是建立统一的用户标识体系把手机号、设备 ID、微信 UnionID 做成映射表极其痛苦每次跑全量要十几个小时但这就是地基地基不牢后面所有的分析都是空中楼阁。我一般会留个心眼拿到任何新数据先按业务主键groupby计数df.groupby(order_id).size().sort_values(ascendingFalse)凡是出现次数大于 1 的抓几条出来瞪眼看三五分钟就能发现是不是重复插入、是不是有父子订单关系比直接上来就建模强一百倍。四、格式地狱没人能活着走出来日期格式这个坑我能单独写本书。2023-02-30都算温柔的我见过2023年2月30日、30/2/2023、Feb 30, 2023同时出现在同一个 CSV 文件里数据源是三个不同国家的运营团队手动填的。处理办法是用pd.to_datetime加errorscoerce但这个只能对付日期的合法性对付不了明天这种中文。为此我专门养了个正则小本本碰到中文日期描述就上规则昨天 运行日期-1明天 运行日期1上个月这种我直接扔给 GPT 的 API 去解析没办法时间成本太高。全角半角问题更是防不胜防。有一回做搜索日志分析发现macbook pro和 全角字符被系统当成两个不同的搜索词导致高频词统计少了一半。后来我养成了习惯所有文本字段进来第一步先做str.normalize(NFKC)这是 Python 标准库unicodedata提供的 Unicode 正规化能把全角字母数字自动转半角省大事。还有空格肉眼看不见的空格。Excel 里复制出来的数据经常带个不间断空格\xa0看着没毛病df[name].unique()一跑发现 “张三” 和 “张三 ” 是两条气得肝疼。我现在数据读进来的第一个 cell 永远是pythonfor col in df.select_dtypes(includeobject).columns: df[col] df[col].str.replace(\xa0, ).str.strip()这行代码救过我的命。五、脏是因为数据从娘胎里就是脏的很多时候我们以为自己在清洗数据其实我们只是在清洗录入环节的排泄物。真正的源头是那个该死的埋点文档是那个根本没做限制的前端表单是那个“随手记一下”的运营后台。我介入过一个 App 改版项目新功能上线后page_view事件突然暴增三倍。产品经理开心地汇报说功能受用户欢迎我一看数据同一个用户在同一秒内会产生几十条一模一样的曝光事件。抓了前端开发一问原来他为了保险把埋点代码写在了视图刷新的回调里列表每划一下可见区域全部控件重新上报一次。这些数据能拿来做行为分析吗不能但产品日报里已经写进 PPT 了。最后我们花了整整两天给事件打上去重逻辑加了时间窗口限制才算勉强把数据掰回到能用的程度。所以我现在跟新人讲得最多的一句话是数据清洗不是建模前的预热动作它是数据从采集那一刻就该开始的生命线。你能往前端表单加一个正则校验就别让后端日志接收垃圾你能让埋点工程师发版前复测一次就别等数据落到数仓里再哭着洗。可惜理想丰满现实是数仓的 ODW 层常年堆满屎山你洗或者不洗模型就在那里被屎淹死。说到底数据清洗考验的不是你会多少 Pandas 骚操作而是你对业务的理解深度和怀疑一切的本能。看到数据先别急着describe先想想这个字段为什么会出现谁填的什么流程产的想不明白就去问问到明白为止。那些你以为的脏可能只是开胃菜真正的硬菜还在后面。但好消息是只要脏数据没把你劝退你就是一个合格的数据矿工——灰头土脸但眼里有光。
数据清洗:你以为的脏,可能只是开胃菜
上周三下午我正端着咖啡刷着技术论坛运营组的小姐姐突然在钉钉上弹我“大神数据我们已经帮你清洗过了直接跑模型就行急今天要结果。” 附带一个 CSV 文件名字叫final_clean_v2_最终版.csv。我放下咖啡满怀期待地pd.read_csv然后df.head()。第一行user_age列赫然写着 “-3”第二行是 “二百”第三行是NULL字符串 —— 注意是字符串 “NULL”不是 NaN。register_date那列更离谱2023-02-30明天还有2020/13/01。我当时一口咖啡直接喷在了屏幕上擦的时候就在想你们管这玩意儿叫清洗过这简直是把拖把在脏水里涮了一下就拿来擦脸。干数据这行久了你就明白一个铁律永远不要相信任何人交给你的数据包括你自己上个月存的。数据清洗这个词被说得太轻巧好像真是把白菜上的泥冲掉那么简单。实际上每一次“清洗”都像是在拆盲盒你永远不知道下一个坑是业务逻辑的扭曲、系统迁移的伤痕还是纯粹的人类手滑。今天这篇我就把那些年踩过的脏数据坑揉碎了讲咱们不列什么“清洗三步法”就聊聊那些让你血压飙升的瞬间和怎么擦干净屁股。一、缺失值从来不是空着的那么简单新人最爱问的一句话缺失值怎么处理教科书上写得明明白白删掉、均值/中位数填充、模型预测填充。实际情况呢你先得搞清楚它为什么缺失。我做过一个线下门店的会员分析项目有个字段叫“婚姻状况”40% 是空值。运营说这些肯定都是单身因为已婚的会填配偶信息。结果我查了一下原始录入日志发现门店的平板系统有个 bug当屏幕键盘弹出来的时候“已婚”选项刚好被挡住收银员懒得滑屏幕直接点了下一步系统就默认跳过了。这 40% 的缺失根本不是单身贵族而是被 UI 坑了的已婚人士。如果随手用众数填“未婚”后面所有家庭型消费的关联挖掘全都会歪掉。所以处理缺失值的第一原则是找到缺失的元凶而不是着急补窟窿。怎么找问业务、看日志、交叉验证。有一次我甚至拉了用户绑定的第三方账号信息发现有些用户手机号为空但在微信 OpenID 里绑着手机只是接口没同步过来这种缺失补起来就有底气。实在查不出来我再教你一个用惯了的粗暴分层法把缺失本身就当成一个特征。比如在预测用户流失时“最近一次登录设备” 缺失往往代表该用户长期没登录或者是远古注册用户根本没激活这个缺失本身的信息量就很大。我会单独建一列is_device_missing然后把原字段用中位数填上放进模型里跑特征重要性往往比你想象的高。再不行你就分桶把缺失作为一个单独类别扔给 CatBoost 这种天然支持缺失值的模型省心。千万别上来就df.dropna()。我曾经亲眼见过一个实习生把包含 30% 缺失值的关键字段直接删掉样本量从 50 万变成 3 万后来我问他为啥他说“网上说缺失多了就得删。” 网上还说一天八杯水呢你喝一个我看看。二、异常值不是数学定义的是业务定义的3σ 原则、箱线图 1.5 倍 IQR这些统计学工具是死的。你拿它去套业务数据能把自己套成傻子。讲个真实案例有次做二手车价格预测车辆行驶里程mileage有不少超过 100 万公里的。用箱线图一看远超上须实习生二话不说当异常值全砍了。我回来检查差点没背过气去——那些全是跑了八九十万公里的营运车辆有几辆出租车改的二手网约车它们的价格逻辑跟普通家用车完全不同是市场上真实存在的交易砍掉它们就等于把整个商用二手车市场给剔除了。最后我们把这些“异常”样本单独建模价格预估准确率直接提了 8 个点。所以异常值不是毒药是情报。处理之前先灵魂拷问这个异常在业务上有可能发生吗如果可能它可能就是你的金矿比如金融风控里的欺诈交易恰恰长着一副异常的面孔如果不可能比如年龄字段冒出来个 245 岁那才是真正的脏数据。我还遇到过用户年龄全是 2023 年的数据集一查发现录入页面年月日三个下拉框默认值是当前年份用户懒得改直接点确认所有懒汉都变成了 2023 年出生。这种情况怎么洗我当时的办法是取该用户第一次消费记录的时间用消费时的年龄倒推出生年虽然没法精确到日但至少把婴儿变成了成年人不至于在亲子类目推荐里给一个 30 岁壮汉推奶粉。说个私藏的土办法对于连续变量我会先画一张足够长的直方图不做任何过滤就盯着尾巴看。有一次在用户注册时长分布里看到尾巴上有几个十亿秒的值算了一下大概是 30 多年而 App 才上线 5 年这百分百是时间戳取错了字段可能是把毫秒当秒存了。这种一眼假的东西写个简单的逻辑df.loc[df[reg_seconds] 5*365*24*3600, reg_seconds] np.nan直接干掉不用纠结。三、重复数据麻烦制造机重复数据里最恶心的一类是“不完全重复”你以为用drop_duplicates就能搞定太年轻。电商订单数据里同一个订单号可能因为用户分两次支付出现了两行一行支付 30一行支付 70总额 100但订单号是同一个。业务上这是同一个订单数据上却是两条记录你直接去重会丢金额不去重会重复计算订单量。这种得先聚合用groupby把金额 sum 起来状态取最新的时间取最早的。还有一种幽灵重复CRM 系统里同一个用户有两个 ID因为人家先在小程序注册又在 App 用手机号注册系统没做打通。他的行为数据散落在两个 ID 下面你分析用户画像时要是没合并就会得出一个分裂的结论这个用户既爱买高端护肤品又狂囤尿不湿以为是个精致奶爸其实人家是个宝妈只不过两个号分别记录了她在不同阶段的购物偏好。处理这种问题的唯一办法是建立统一的用户标识体系把手机号、设备 ID、微信 UnionID 做成映射表极其痛苦每次跑全量要十几个小时但这就是地基地基不牢后面所有的分析都是空中楼阁。我一般会留个心眼拿到任何新数据先按业务主键groupby计数df.groupby(order_id).size().sort_values(ascendingFalse)凡是出现次数大于 1 的抓几条出来瞪眼看三五分钟就能发现是不是重复插入、是不是有父子订单关系比直接上来就建模强一百倍。四、格式地狱没人能活着走出来日期格式这个坑我能单独写本书。2023-02-30都算温柔的我见过2023年2月30日、30/2/2023、Feb 30, 2023同时出现在同一个 CSV 文件里数据源是三个不同国家的运营团队手动填的。处理办法是用pd.to_datetime加errorscoerce但这个只能对付日期的合法性对付不了明天这种中文。为此我专门养了个正则小本本碰到中文日期描述就上规则昨天 运行日期-1明天 运行日期1上个月这种我直接扔给 GPT 的 API 去解析没办法时间成本太高。全角半角问题更是防不胜防。有一回做搜索日志分析发现macbook pro和 全角字符被系统当成两个不同的搜索词导致高频词统计少了一半。后来我养成了习惯所有文本字段进来第一步先做str.normalize(NFKC)这是 Python 标准库unicodedata提供的 Unicode 正规化能把全角字母数字自动转半角省大事。还有空格肉眼看不见的空格。Excel 里复制出来的数据经常带个不间断空格\xa0看着没毛病df[name].unique()一跑发现 “张三” 和 “张三 ” 是两条气得肝疼。我现在数据读进来的第一个 cell 永远是pythonfor col in df.select_dtypes(includeobject).columns: df[col] df[col].str.replace(\xa0, ).str.strip()这行代码救过我的命。五、脏是因为数据从娘胎里就是脏的很多时候我们以为自己在清洗数据其实我们只是在清洗录入环节的排泄物。真正的源头是那个该死的埋点文档是那个根本没做限制的前端表单是那个“随手记一下”的运营后台。我介入过一个 App 改版项目新功能上线后page_view事件突然暴增三倍。产品经理开心地汇报说功能受用户欢迎我一看数据同一个用户在同一秒内会产生几十条一模一样的曝光事件。抓了前端开发一问原来他为了保险把埋点代码写在了视图刷新的回调里列表每划一下可见区域全部控件重新上报一次。这些数据能拿来做行为分析吗不能但产品日报里已经写进 PPT 了。最后我们花了整整两天给事件打上去重逻辑加了时间窗口限制才算勉强把数据掰回到能用的程度。所以我现在跟新人讲得最多的一句话是数据清洗不是建模前的预热动作它是数据从采集那一刻就该开始的生命线。你能往前端表单加一个正则校验就别让后端日志接收垃圾你能让埋点工程师发版前复测一次就别等数据落到数仓里再哭着洗。可惜理想丰满现实是数仓的 ODW 层常年堆满屎山你洗或者不洗模型就在那里被屎淹死。说到底数据清洗考验的不是你会多少 Pandas 骚操作而是你对业务的理解深度和怀疑一切的本能。看到数据先别急着describe先想想这个字段为什么会出现谁填的什么流程产的想不明白就去问问到明白为止。那些你以为的脏可能只是开胃菜真正的硬菜还在后面。但好消息是只要脏数据没把你劝退你就是一个合格的数据矿工——灰头土脸但眼里有光。