AI数据清洗实践:用Pandas解决脏数据难题
2026/9/15 21:46:57 网站建设 项目流程

做AI的朋友应该都听过一句话:Garbage in, garbage out。模型结构再先进,训练数据是脏的,结果也好不到哪去。在AI基础设施与生态层里看,Data(数据)是整个链条的燃料,而数据清洗就是给燃料把关的那个环节。这个事我这些年踩过不少坑,也见过太多项目在建模阶段被脏数据坑得死去活来。这篇博文不绕弯子,直接把“数据清洗”这件事掰开揉碎,讲清楚它为什么关键、到底要处理哪些问题、用什么工具可以做、以及有哪些容易翻车的地方。适合正在搭数据管道的数据工程师、准备做特征工程的算法工程师,以及对AI基础设施有兴趣但还没上手接触脏数据的入门同学。

1. 为什么说数据清洗是AI基础设施里的关键环节

1.1 脏数据比你想的更常见

先看几个真实环境里常见的脏数据长什么样:

  • APP埋点上报的日志里,混着content://com.tencent.wework.fileprovider/external_path/android/data/com...这样的文件路径串;
  • 网页爬虫抓回来的内容,开头是data:text/html;base64,...这种带前缀的完整HTML片段;
  • 工业传感器回传的数据里,一条记录的时间戳是13位毫秒,下一条突然变成10位秒,还有几条时间戳直接是乱序的;
  • 数据库导出文件里,同一个用户ID在“用户ID”字段下既有数字,又有"NULL"字符串,还有一两个空单元格。

这些不是段子,是我在数据接入阶段反复看到的事。数据处理领域有一句经典的话:真实世界的数据永远是脏的。原因不复杂——数据来自不同业务系统、不同采集终端、不同接口版本,再加上人为录入错误、网络抖动丢包、日志格式变更,数据一汇到一起,各种问题全都会浮出来。

很多人以为数据清洗只是“写几个正则把乱码过滤掉”,实际上远不止这些。在AI基础设施里,数据清洗决定了下游特征工程能不能做、模型能不能训得动。数据管道里的第一个环节如果糊弄,后面每一步都会放大错误。

1.2 不做清洗的代价到底有多大

业内流传很广的说法是:一个数据科学项目里,数据准备和清洗要占掉七八成时间,真正建模只占两成。我没精确统计过,但从实际项目看,这个数字不离谱。

如果跳过清洗直接建模,最直接的后果是模型性能下降。缺失值会让某些树模型产生偏置,异常值会拉偏损失函数,重复样本会影响样本权重的分布,格式不一致会让特征拼接直接报错。更隐蔽的是训练分布和线上分布不一致的问题——训练时用了带大量异常尖峰的传感器数据,模型会把这些尖峰当成正常模式;上线后真实数据稍有波动,模型就会误判。

在AI基础设施层面,脏数据还会造成计算资源浪费。我在一个推荐系统项目里见过,因为上游日志里出现了大量重复记录,每天的清洗任务跑完后,特征平台仍然写入了几百万条几乎一样的样本,后面训练任务的时间直接翻倍。这种问题如果早期不查,排查成本会非常高。

1.3 AI基础设施对数据质量提出了新要求

过去讲“数据清洗”,可能只是数据分析师的日常琐事。但现在AI基础设施已经形成了数据存储、计算引擎、调度系统、特征平台、模型训练和推理服务的完整生态,数据清洗不再是可有可无的脚本,而是基础设施里的一个标准环节。

基础设施要求数据清洗具备三个特性:

  • 可复用:相同的清洗逻辑不能在多个下游里各写一遍,要沉淀成公共模块;
  • 可观测:每次清洗执行之后,要能记录多少行被过滤、多少字段被填充、质量指标变化如何;
  • 可回溯:一条数据在管道里被改过什么,要能通过版本和日志追踪到。

这也是为什么现在很多团队会把清洗逻辑放在离在线服务更近的位置,做成独立的清洗服务,而不是藏在某个模型训练代码里。数据和AI基础设施的关系就像燃料和发动机,数据清洗就是燃料精炼工序,精炼质量直接决定发动机能不能稳定运转。

2. 数据清洗与数据预处理,别把概念搞混

2.1 清洗和预处理的分工不一样

聊数据清洗时,总有人把它和数据预处理混着说。我的理解是,两者有重叠,但目标不同。

数据清洗解决的是“数据对不对”的问题,重点在修复:缺失值补不补、重复值删不删、异常值怎么处理、日期格式统不统一、编码乱不乱、同一实体在不同表里指代是否一致。

数据预处理解决的是“数据能不能喂给模型”的问题,重点在变换:特征标准化、归一化、分箱、类别编码、降维、样本划分这些。

通常一条数据处理管道是:原始数据进来,先做清洗,再做预处理。清洗做得干净,预处理才有效。如果原始日期字段里混着2024/01/012024-01-0120240101三种格式,不先清洗统一,后面的时间特征提取就没法做。

2.2 数据清洗要管的四类核心问题

我把日常数据清洗工作归结成四个大类,几乎覆盖了大多数场景。

问题类型常见表现典型处理方式
缺失值NaN、空字符串、字符串“NULL”、0值占位删除、均值/中位数填充、前向填充、模型预测填充、单独标记
重复值整行完全重复、业务主键重复、部分字段重复按业务主键去重,保留最新或保留质量最高记录
异常值超过业务范围、超过N个标准差、IQR离群点截断、剔除、单独建模、分箱处理
格式不一致日期格式混乱、时间戳单位不统一、编码乱码、单位不统一统一格式、统一单位、类型转换、正则规则替换

这些只是入口分类。实际业务里,四类问题经常叠加在一起。比如一个传感器数据表,同一列里既有缺失值,又有超过量程的上限值,还有重复上报的记录,处理顺序就很重要。一般我习惯的顺序是:先做格式统一和类型转换,再做去重,然后处理缺失值,最后处理异常值。原因很简单,格式没统一时,很多“重复值”其实是因为格式不同没被识别出来。

2.3 一个具体案例:工业传感器数据清洗

热词里有“工业传感器数据清洗”,这块我专门聊过。传感器数据和其他数据最大的不同是带有时间序列属性,清洗时不能仅按单条记录判断,还要看上下文。

比如温度传感器可能出现一个突变尖峰:前一条读数25.4度,当前条35.2度,下一条又回到25.3度。从单点看,35.2并没有超量程上限;但从序列看,这就是典型的突跳噪声。处理这种问题,简单方式是计算当前点与前一点、后一点的差值,超过业务阈值就判定为异常,然后用滑动窗口的中位数替换。

工业场景里还有一个容易踩坑的点:传感器数据经常带状态码。同样一个数值,状态码是“正常”还是“校准中”,可信度完全不同。清洗时不能只盯数值,还得把状态码一起考虑,不然很容易把正常数据当成异常删掉。

3. 用Pandas做数据清洗的完整实操

3.1 数据读进来之后先看一眼,不要急着动手

很多人拿到一个CSV,第一件事就是df.dropna(),这是最不推荐的。我自己的习惯是清洗之前至少花几分钟做“体检”。

import pandas as pd df = pd.read_csv("raw_data.csv", encoding="utf-8") print(df.shape) print(df.info()) print(df.head(10)) print(df.describe(include="all"))

df.info()能快速看到每列的非空数量和数据类型,df.describe()能看数值列的分布。这两个操作不会修改数据,但能帮你判断问题大概集中在哪些列。

还有一个容易被忽略的点:读文件时最好显式指定dtypeencoding。不指定的话,Pandas 可能会把用户ID或手机号识别成数字,把有前导零的编码给丢掉。我之前处理过一批订单号,因为没指定类型,数值型订单号把开头的0全部吞了,后面再做关联匹配时全是脏的。

3.2 缺失值处理:不要无脑 dropna 或 fillna

缺失值处理是清洗大头,但处理方式要看数据量、缺失比例和业务含义。

# 查看每列缺失比例 missing_ratio = df.isna().mean() print(missing_ratio) # 缺失比例极低的行,可以直接删 df_cleaned = df.dropna(subset=["user_id", "order_time"]) # 数值列缺失,用中位数填充 df["amount"].fillna(df["amount"].median(), inplace=True) # 时间序列列缺失,用前向填充 df["sensor_value"].fillna(method="ffill", inplace=True) # 缺失本身就代表一种状态时,单独标记 df["has_amount"] = df["amount"].isna().astype(int)

这里有几个经验:

  • 中位数比均值稳健。数据有偏斜时,均值容易被极值带偏,直接用均值填充反而会让整体分布失真。
  • 时间序列里的缺失,优先考虑前向填充或插值,而不是用全局均值填充。
  • 如果一列缺失比例超过70%,除非业务上有明确说法,否则我一般会直接丢列。硬填出来的字段不仅没价值,还会引入噪声。

3.3 去重:看清逻辑主键再删

去重不是简单地drop_duplicates(),关键是先想清楚“重复”是按什么判断的。

# 完全重复行 df.drop_duplicates(inplace=True) # 按业务主键去重,保留最新记录 df.drop_duplicates(subset=["user_id", "biz_date"], keep="last", inplace=True) # 保留质量更高的一条:比如优先保留备注不为空的行 df["remark_flag"] = df["remark"].notna().astype(int) df.sort_values("remark_flag", ascending=False, inplace=True) df.drop_duplicates(subset=["device_id"], keep="first", inplace=True)

实际业务里,同一个用户一天有多次点击,但这不算重复;同一张订单在两个系统里各同步了一份,才算重复。判断标准完全来自业务口径,不能光靠统计学。

3.4 异常值处理:先圈范围再动刀

异常值检测的常用方法有三种:业务阈值、Z-score、IQR。

# 业务阈值:比如商品价格不能为负 df = df[df["price"] >= 0] # Z-score:超过3个标准差视为异常 from scipy import stats z = stats.zscore(df["sensor_value"]) df = df[(z.abs() < 3)] # IQR:超出四分位距1.5倍视为异常 Q1 = df["amount"].quantile(0.25) Q3 = df["amount"].quantile(0.75) IQR = Q3 - Q1 df = df[(df["amount"] >= Q1 - 1.5 * IQR) & (df["amount"] <= Q3 + 1.5 * IQR)]

Z-score 和 IQR 都只能做粗筛,最后还是要结合业务判断。比如看用户年龄时,超过3个标准差的记录可能是100岁老人,也可能是录入错误,直接删除前最好人工抽查几条。我遇到过一种情况:某个渠道的客单价天然偏高,用全局IQR会把正常的高价值用户当作异常剔除,后来改成按渠道分组检测,问题才解决。

3.5 类型和格式清洗:日期、字符串、单位

清洗日期和时间戳是高频操作。

# 统一日期格式 df["biz_date"] = pd.to_datetime(df["biz_date"], format="%Y-%m-%d", errors="coerce") # 处理时间戳:13位毫秒 df["ts_ms"] = pd.to_datetime(df["ts_ms"], unit="ms") # 处理时间戳:10位秒 df["ts_s"] = pd.to_datetime(df["ts_s"], unit="s") # 字符串清理:去空格、去全角空格、统一大小写 df["user_name"] = df["user_name"].str.strip().str.replace("\u3000", "") df["status"] = df["status"].str.lower() # 单位统一:把金额统一为元 df["amount"] = df["amount"].apply(lambda x: x / 100 if x > 10000 else x)

这里特别提醒:pd.to_datetime里加一个errors="coerce",解析不了的日期会变成NaT,方便下一步统一处理。如果不加,解析遇到异常格式时整个程序会直接报错,清洗任务就崩了。

3.6 文本脏数据清洗:正则处理日志和HTML片段

现在很多数据清洗任务要处理非结构化文本。比如埋点日志里的完整文件路径、带协议的HTML片段、包含base64的图片串。这种内容不能整条删除,而是要按结构抽取有用字段。

import re df["raw_text"] = df["raw_text"].astype(str) # 提取形如 content:// 的协议前缀 df["uri_protocol"] = df["raw_text"].str.extract(r"(content://[a-zA-Z0-9.]+)") # 剔除 data:text/html 等前缀,保留正文部分 df["html_body"] = df["raw_text"].str.replace(r"^data:text/html[^,]*,", "", regex=True) # 去掉URL和文件路径 df["clean_text"] = df["raw_text"].str.replace(r"(https?://\S+|/storage/emulated/\d+/[^\s]+)", "", regex=True) # 过滤掉纯乱码的base64片段 df["clean_text"] = df["clean_text"].apply(lambda x: re.sub(r"[A-Za-z0-9+/]{100,}={0,2}", "[BASE64]", x))

这种清洗逻辑没有标准答案,完全依赖你对数据源的理解。我的经验是:先拿100条样例,把能穷举的格式都列出来,写正则时一条一条过;不要上来就写一个大而全的正则,容易被特殊情况打脸。

4. 数据清洗规则如何沉淀成自动化工序

4.1 清洗规则不能是一次性脚本

很多团队最开始就是写一个clean.py,跑完就扔。这个思路在短期项目里能应付,但一旦数据源更新、业务逻辑变化,脚本很快就不可维护了。

我更推荐把清洗逻辑拆成函数,每个函数只负责一类问题,然后统一编排。

def clean_duplicates(df): before = df.shape[0] df = df.drop_duplicates(subset=["order_id"], keep="last") after = df.shape[0] return df, {"duplicates_removed": before - after} def clean_missing(df): df["amount"] = df["amount"].fillna(df["amount"].median()) return df, {"missing_filled": int(df["amount"].isna().sum())} def clean_outliers(df): Q1 = df["amount"].quantile(0.25) Q3 = df["amount"].quantile(0.75) IQR = Q3 - Q1 before = df.shape[0] df = df[(df["amount"] >= Q1 - 1.5 * IQR) & (df["amount"] <= Q3 + 1.5 * IQR)] return df, {"outliers_removed": before - df.shape[0]} def run_data_cleaning(df): reports = [] for fn in [clean_duplicates, clean_missing, clean_outliers]: df, report = fn(df) reports.append(report) print(reports) return df

把每个清洗动作封装成独立函数,日后加规则只需新增函数,不用动主流程。规则变化时,也能通过函数版本记录是谁、在什么时候改的。

4.2 数据质量检查:清洗前后都要做

清洗之后,必须检查效果。没有质检的清洗,和没洗一样。

def quality_report(df): report = { "total_rows": df.shape[0], "total_cols": df.shape[1], "missing_ratio": df.isna().mean().to_dict(), "duplicate_rows": int(df.duplicated().sum()), "numeric_range": {col: [float(df[col].min()), float(df[col].max())] for col in df.select_dtypes("number").columns}, } return report print("清洗前:", quality_report(df_raw)) df_cleaned = run_data_cleaning(df_raw) print("清洗后:", quality_report(df_cleaned))

实际生产环境里,我会把这份质检报告输出到日志系统或数据库,这样调度平台能看到每天清洗的质量变化。如果某天缺失率突然从1%涨到30%,大概率是上游数据源出问题了。

4.3 工具选型:Pandas之外还能用什么

数据清洗不只有Pandas。我按场景整理了一份常用工具对比:

工具适用场景特点
Pandas中型数据、快速探索、规则复杂灵活、生态好,适合Python团队
DataX数据同步过程中做字段过滤和简单转换解耦性好,适合离线数据接入管道
Pentaho Data Integration图形化ETL、非Python团队无需写代码,但大规模处理性能一般
OpenRefine探索式清洗、脏数据可视化上手快,适合小样本数据调试规则
Spark DataFrame海量数据、分布式清洗能横向扩展,但代码门槛高

我个人的习惯是:单机内存放得下的数据,优先用Pandas;数据量几十GB以上再切Spark。DataX更适合做数据同步,不建议拿它做复杂的清洗逻辑,它擅长的是把数据从一个地方搬到另一个地方时顺手做点简单转换。

4.4 工业级流水线里的位置

在实际的AI基础设施流水线里,数据清洗的位置通常是在数据接入之后、特征工程之前。

离线部分一般是:数据源 → DataX同步 → 清洗任务 → 数据质量检查 → 写入数仓/特征库 → 特征工程。

在线部分则要求清洗逻辑更精简,通常只能做字段校验、格式修正和异常值截断,复杂的规则要放到离线的训练数据管道里做。原因是在线推理对延迟敏感,不可能为了清洗一整段历史数据而等几秒。

还有一个建议:清洗规则里的阈值、字段名尽量不要硬编码在代码里,可以放到配置中心或规则表里。这样业务改了阈值,数据团队不用重新部署代码,改个配置就能生效。我在团队里就是把清洗规则统一做成配置文件,每天定时拉取,管理起来省心很多。

5. 常见问题与排查技巧实录

5.1 埋点日志里的路径串和HTML片段怎么处理

处理APP埋点日志时,经常碰到content://.../storage/emulated/0/android/data/...data:text/html;base64...这类内容。它们混在业务字段里,如果不处理,后续做文本特征时会引入大量噪声。

我的处理顺序是:

  1. 先把原始值规范化成字符串;
  2. 用正则抽取协议名、路径主目录、文件名后缀等结构化信息;
  3. 如果原始内容对业务无用,直接过滤掉;
  4. 如果里面的base64或HTML是业务关注点,先解码再清洗,解码失败就标记为不可用。

一定不要用一条正则就妄图搞定所有情况。埋点格式经常因为SDK版本不同而变化,所以给每条清洗规则加上版本号和管理人,比追求一次性完美规则更实际。

5.2 编码问题和乱码,永远排在清洗第一位

读文件报UnicodeDecodeError,或者是输出后中文变“锟斤拷”,这是所有数据清洗新手的老朋友。

解决办法:

# 方案1:尝试用可能编码逐个读 for enc in ["utf-8", "gbk", "gb2312", "latin1"]: try: df = pd.read_csv("raw.csv", encoding=enc) print("读取成功,编码:", enc) break except UnicodeDecodeError: continue # 方案2:用 chardet 检测编码 import chardet with open("raw.csv", "rb") as f: raw = f.read(100000) result = chardet.detect(raw) df = pd.read_csv("raw.csv", encoding=result["encoding"])

真实项目里,从Windows导出的CSV经常是GBK或GB2312,而网页爬下来的内容是UTF-8。最好的做法是在清洗脚本里做一次编码检测,自动适配来源。

5.3 日期时区和时间戳的坑,最容易栽跟头

我发现很多清洗事故都出在时间戳上。13位毫秒和10位秒混用很常见,还有带时区的ISO字符串,比如2024-06-01T00:00:00+08:00。新手直接用pd.to_datetime转,得到的结果常常带时区偏移,和另一个不带时区的表做关联时就对不上。

我的建议是:统一转成UTC时间戳,或者统一转成无时区的北京时间。

df["event_time"] = pd.to_datetime(df["event_time"], utc=True) df["event_time_beijing"] = df["event_time"].dt.tz_convert("Asia/Shanghai").dt.tz_localize(None)

如果原始数据里既有10位秒又有13位毫秒,先判断位数再转换。

df["ts_digit_len"] = df["ts"].astype(str).str.len() df["dt"] = pd.to_datetime(df["ts"], unit="s") # 之后再针对13位的重转

5.4 空字符串、NaN和None,判断起来不是一回事

Pandas里NoneNaN和空字符串""看起来像一回事,实际处理完全不同。df.isna()只对NaNNone返回True,""不算缺失。但真实数据里,空字符串往往也代表“没填”。

处理办法是把常见“伪缺失”统一转成真正的缺失值。

df = df.replace(r"^\s*$", pd.NA, regex=True) df = df.replace("NULL", pd.NA) df = df.replace("null", pd.NA) df = df.replace("None", pd.NA)

这个步骤最好在读取后立刻执行,否则后面的缺失值统计就会失真。

5.5 数据清洗里的合规和隐私保护

最后说一个容易被忽略的问题:清洗过程也会接触到敏感数据。比如手机号、身份证号、地址、健康指标这类字段,清洗和调试时经常需要看样例。

我的做法是:

  • 开发环境里先用脱敏后的样例数据,不走全量明文;
  • 清洗脚本中内置一个脱敏函数,对手机号、邮箱做掩码处理;
  • 日志里不要打印完整明细字段,只打印行列数和统计指标;
  • 清洗后的数据落地到仓库时,控制访问权限和流转范围。

数据清洗不只是技术活,它同时也是在给整个AI基础设施建立数据安全的边界。规范做在前面,后面能省很多事。

我自己在实际操作中还有一个习惯:每次清洗任务跑完后,除了质量报告,顺手把“这次清洗用了哪些规则、每个规则删了多少行、填充了多少值”记到一张统计表里。一个月下来回头翻一翻,就能看出上游数据质量在变好还是变坏,也能知道该跟哪个团队沟通格式变更。这个过程不需要太复杂,一个脚本加一张表就够了。数据清洗这东西,做得越细,后面建模和上线的日子就越好过。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询