有次帮学弟看网约车大数据的毕业设计,他把订单表、司机表、轨迹表全塞进MySQL,然后告诉我“数据已经弄好了,就差跑分析了”。我打开一看,司机表的城市字段里有“北京市”“北京”“beijing”三种写法,订单金额里混着“元”和“万元”,更夸张的是轨迹表的时间戳居然有13位的毫秒数也有10位的秒数。那一瞬间我意识到,大多数人说的“数据准备好了”,和真正可以分析的数据之间,隔着整整一个数据清洗。
这件事也正好印证了我在数据行业里摸爬滚打多年的一个判断:大数据项目真正耗时的,从来不是建模和可视化,而是数据清洗。很多刚学大数据的朋友,拿到Excel文档、Pandas教程、Spark实验,第一反应是“清洗而已,dropna一下不就好了”,但真实场景里的清洗远不是这么回事。这篇内容,我就把数据清洗在大数据领域里的位置、工具选型、实操思路和踩过的坑,一次说清楚。
1. 数据清洗不是“预处理”,而是数据项目的大头
我先说一个很多人不爱听的事实:数据清洗不是数据流程里一个可以“快速过一下”的预处理环节,它本身就是一个项目的主干工作。业内流传比较广的说法是“数据项目里70%的时间花在清洗和准备数据上”,我个人实践下来觉得这还保守了,有些源系统质量差的场景,清洗时间能占到90%。
1.1 为什么教程从不讲脏数据
你有没有发现一个奇怪的现象:教科书和网课里的数据永远干干净净,索引整齐,没有缺失值,日期格式统一,一跑就出结果。但真实世界里,数据是这个样子的:同一个用户ID在注册表里是纯数字,在行为日志里却带了“U_”前缀;电商订单的下单时间有“2024-05-01 10:00:00”,也有“20240501100000”,甚至还有“2024/05/01”;你left join两张表,本意是查遗漏,结果主键重复导致行数膨胀了一倍。
为什么教程不讲这些?因为讲脏数据太“不优雅”,而且干净的数据才能让知识点聚焦。但现实是,不会清洗数据的人,再好的分析模型都是白搭。就像做饭,菜不洗不切,锅再好也出不了菜。
1.2 数据清洗到底在洗什么
我在一线做数据项目时,会把数据清洗拆成四类问题来对待,这样不容易漏:
- 格式类问题:字段类型不对、单位不一致、日期格式混乱、编码不统一。这类问题最容易修,但也最容易忽视。
- 内容类问题:缺失值、重复值、异常值、非法值。这里要特别注意“看起来正常但实际是脏数据”的情况,比如某个字段填了“-99”表示缺失,或者用“0”占位。
- 结构类问题:多表关联时主外键不匹配、一张表里塞了多个业务维度、同一实体在不同表中的标识不一致。
- 业务类问题:数据本身合法,但不符合业务逻辑。比如下单时间早于注册时间、退款金额大于订单金额、GPS轨迹点瞬间跨越两个城市。
只有把这四类问题都处理干净了,后续的数据分析、可视化、建模才有意义。反过来,如果你想判断一个数据工程师的段位,第一步不是看他模型跑得多溜,而是看他面对一张脏表时,能不能系统性地把问题列出来、处理掉、并给出清洗报告。这也是面试官最爱考察的能力点。
2. 从Excel到Pandas:单机数据清洗的入门路径
很多学生朋友最初接触数据分析,都是从Excel开始的,这也是为什么热词里会有“大数据人工智能时代与学生本人所学专业excel文档”。Excel本身确实是很好的清洗工具,尤其对于MB级别的数据,透视表、筛选、替换功能都很顺手。但它有个天然的瓶颈:数据量一大,比如超过几十万行,Excel就会卡成PPT。这时候就要迁移到Pandas。
2.1 处理规模决定工具选型
我给个非常务实的选型标准,你可以直接套用:
| 数据规模 | 推荐工具 | 理由 |
|---|---|---|
| 100MB以内 | Excel / Pandas | 单机内存完全扛得住,Excel操作直觉,Pandas可脚本化 |
| 100MB~10GB | Pandas / Dask | 单机能勉强处理,但需要分批或并行;Pandas生态最成熟 |
| 10GB以上 | Spark / Flink | 必须上分布式,单机内存装不下,需要多节点并行 |
注意这个表是“内存数据量”,不是“磁盘文件大小”。CSV文件可能只有2GB,但读入Pandas后经过中间运算,内存占用可能飙升到6GB甚至更高,所以判断时一定要留出余量。
2.2 一份点击流日志的Pandas完整清洗流程
我这里用一个点击流日志的例子,带你走一遍完整的Pandas清洗流程。假设你拿到一份用户行为日志,字段有user_id、visit_time、page_url、device_type、session_id,总共80万行。原始数据长这样:
U_1001,2024/05/01 10:23:45,/home,Android,abc123 U_1001,2024-05-01 10:25:12,/product/123,Android,abc123 1002,2024-05-01 10:30:01,/cart,ios,, U_1003,20240501104530,/checkout,IOS,def456 U_1004,2024-05-01 10:50:33,/home,PC,ghi789第一步是读入并做概览检查,不要上来就埋头处理:
import pandas as pd df = pd.read_csv("click_log.csv", header=None, names=["user_id", "visit_time", "page_url", "device_type", "session_id"]) print(df.shape) print(df.dtypes) print(df.head(10))这一步就能发现很多问题:visit_time是object类型说明日期没被解析,user_id有“U_”前缀也有纯数字,device_type大小写不一致,session_id有空值。
第二步是统一格式,把非标准的日期字符串揉成一个标准格式:
df["visit_time"] = pd.to_datetime(df["visit_time"], errors="coerce")这里errors="coerce"很关键,它会把解析不了的日期变成NaT而不是直接报错中断,方便你后面统一统计到底有多少坏值。
第三步是处理用户ID前缀:
df["user_id"] = df["user_id"].astype(str).str.replace("U_", "", regex=False) df["user_id"] = pd.to_numeric(df["user_id"], errors="coerce")但注意,用户ID也不能光替换完就收工。替换完后你还要检查一下有没有负数、0、异常大的数,这些往往是“测试账号”或者“程序bug”产生的,需要单独过滤。
第四步是设备类型归一化,把“ios”“IOS”“iOS”统一成“iOS”:
df["device_type"] = df["device_type"].str.lower().str.strip()第五步是处理重复值。这里我强调一下,重复值不是无脑drop_duplicates()就完事。你要先判断业务上什么是重复:是整个会话记录都重复了,还是同一用户在几秒内点了同一个URL算重复?判断标准不同,处理方式也不同。基础版是全字段去重:
df = df.drop_duplicates()进阶版是“按user_id+time”去重,保留最后一条:
df = df.sort_values("visit_time").drop_duplicates(subset=["user_id", "visit_time"], keep="last")第六步是异常值处理。比如device_type里的“unknown”“null”之类的占位字符串,和真正的空值要分开对待:
df["device_type"] = df["device_type"].replace({"unknown": None, "null": None, "": None})这样一个清洗流程跑下来,你会得到一份干净、口径统一、类型正确的DataFrame,而不是“看起来好像能跑”的数据。
2.3 高频清洗操作速查
我整理了一些日常最常用到的Pandas清洗手段,方便你写脚本时直接查:
| 需求 | 代码示例 | 说明 |
|---|---|---|
| 删除全空列 | df.dropna(axis=1, how="all") | 整列都空才删 |
| 空值填充 | df.fillna({"device": "unknown"}) | 按列指定填充值 |
| 类型转换 | df["price"] = df["price"].astype(float) | 转换前先检查有无非法值 |
| 字符串截取 | df["city"] = df["address"].str[:2] | 提取城市码 |
| 去除首尾空格 | df["name"] = df["name"].str.strip() | 常见但容易被忽略 |
| 统一大小写 | df["city"] = df["city"].str.lower() | 归一化文本 |
| 条件替换 | df["status"].replace({"1": "active", "0": "inactive"}) | 编码映射 |
| 区间截断 | df = df[(df["age"] >= 0) & (df["age"] <= 120)] | 过滤合理区间 |
| 百分位去极值 | lower = df["amount"].quantile(0.01);df = df[df["amount"] >= lower] | 去掉极端离群点 |
2.4 编码问题:最容易忽略的深坑
Pandas清洗里还有一个特别容易踩的坑:编码。CSV文件最常见的utf-8和gbk两种编码,有时候还会遇到utf-8-sig。如果你读文件出来一堆乱码,八成是编码不对。一个通用的处理思路是先尝试用二进制模式读前几行探测,或者直接让pandas自动尝试:
try: df = pd.read_csv("data.csv", encoding="utf-8") except UnicodeDecodeError: df = pd.read_csv("data.csv", encoding="gbk")实测下来,国内很多业务系统导出的CSV默认都是GBK,尤其是老系统。如果你用utf-8读取会直接报错,但如果你用gbk读取还有可能遇到“gbk编解码器无法解码”的问题,这时候可以换成gb18030,它是GBK的超集,兼容性更好。
3. 进入分布式:Spark和MapReduce里的清洗逻辑
单机Pandas应付教学和中小型项目够了,但一到网约车轨迹、招聘数据、农产品价格这种动辄几千万行的数据,你就不得不面对分布式。注意,分布式清洗不是简单地把Pandas代码丢到Spark上跑,而是整个思路都要变。
3.1 为什么不能用“大数据版Pandas”的思路
Pandas的核心优势是DataFrame在内存里,你可以随意order、lambda、复杂循环,所有行都在你面前。但分布式环境你面对的是成百上千个partition,数据被切散到不同节点上,你要思考的问题是:每个分区里的数据能独立做什么,哪些操作必须跨分区重排。
举个例子,Pandas里你可以把当前行的日期和上一行的日期做差值,判断用户是否在短时间内重复访问。但在Spark里,这种“按行顺序逐行计算”的操作代价极高,必须用窗口函数指定partitionBy和orderBy才能高效实现。很多新手不懂这个区别,写着写着就出了一大堆shuffle,跑一次清洗任务半小时起。
所以进入分布式之前,我建议你先建立三个认知:
- 能一次扫描解决的,绝不做二次扫描。读一遍原始数据,同时完成格式转换、字段校验、空值标记,第二次扫描只做汇总统计,效率最高。
- 能用算子解决的,绝不写UDF。自定义UDF在Spark里会破坏内置优化,除非业务逻辑实在复杂,否则优先用map、filter、withColumn这些内置算子。
- 能小表广播的,绝不做大表join。维度表、映射表这种几百MB以内的数据,用broadcast join广播到各节点,避免大表之间shuffle。
3.2 MapReduce清洗招聘数据的经典模式
还记得热词里有“实验4 mapreduce综合应用案例 — 招聘数据清洗”和“网约车大数据综合项目——基于mapreduce的数据清洗”吗?MapReduce作为老牌分布式计算模型,在清洗场景里有一个非常经典的模式:Map阶段做清洗,Reduce阶段做聚合。
拿招聘数据举例。假设你从招聘网站上抓了一批原始数据,字段里有职位名称、薪资范围、城市、发布日期。脏问题集中在:城市字段有“北京”“北京市”“北京(总部)”多种写法,薪资有“8k-15k”“10万/年”“面议”等格式,发布日期有“2024-05-01”“05/01/2024”各种花样。
Map阶段的职责就是逐行清洗:
public class CleanMapper extends Mapper<LongWritable, Text, Text, Text> { @Override protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException { String line = value.toString(); String[] fields = line.split(","); String city = normalizeCity(fields[2]); // 把北京/北京市/北京(总部)映射成统一值 String salary = normalizeSalary(fields[5]); // 把8k-15k解析出min和max两个数值字段 String publishDate = normalizeDate(fields[6]); // 各种日期格式统一成yyyy-MM-dd String cleanRecord = String.join(",", fields[0], fields[1], city, salary, publishDate); context.write(new Text(fields[1]), new Text(cleanRecord)); } }Reduce阶段做的是聚合过滤,比如同一个职位ID的重复投递只保留第一份,或者统计每个城市的平均薪资范围:
public class CleanReducer extends Reducer<Text, Text, Text, Text> { @Override protected void reduce(Text key, Iterable<Text> values, Context context) throws IOException, InterruptedException { // 按职位ID去重、合并、输出最终的清洗结果 } }MapReduce清洗有个好处:它是“写一次跑永久”的批处理模式,天然适合每天定时跑,把前一天抓取的增量数据清洗后灌入数据仓库。这也是很多老牌数仓项目还在用的原因。
3.3 Spark清洗网约车轨迹数据实战片段
近几年的网约车大数据综合项目,基本都改成用Spark了。我拿轨迹数据清洗举个例子,这类数据最大的特点是量超大、GPS漂移严重、时间戳格式混乱。
假设原始字段是:order_id、driver_id、gps_time、longitude、latitude。清洗目标有三个:时间统一、过滤漂移点、按订单分段。
时间统一用Spark SQL就能搞定:
from pyspark.sql import SparkSession from pyspark.sql.functions import col, to_timestamp spark = SparkSession.builder.appName("trajectory_clean").getOrCreate() df = spark.read.csv("track_logs/", header=True) # 统一时间字段:兼容毫秒时间戳和标准字符串 df = df.withColumn("gps_time", to_timestamp(col("gps_time"), "yyyy-MM-dd HH:mm:ss"))GPS漂移过滤就稍微有点讲究。网约车的GPS轨迹在隧道、高架桥下经常会蹦出离谱的点,比如瞬时速度算出来超过300km/h,或者在相隔1秒内横跨两个城市。这种点如果不洗掉,后续计算行驶里程、平均车速都会失真。
我的做法是用速度阈值过滤:根据经纬度差值算出两点之间的距离,再除以时间差得到瞬时速度,超过120km/h的点直接标记为漂移点删掉。这里用Spark窗口函数按order_id分组、按时间排序,取上一行的时间和经纬度来计算:
from pyspark.sql.functions import lag, radians, sin, cos, asin, sqrt, lit, when # 按订单分组,按时间排序,取前一行经纬度 df = df.withColumn("prev_lon", lag("longitude").over(Window.partitionBy("order_id").orderBy("gps_time"))) df = df.withColumn("prev_lat", lag("latitude").over(Window.partitionBy("order_id").orderBy("gps_time"))) df = df.withColumn("prev_time", lag("gps_time").over(Window.partitionBy("order_id").orderBy("gps_time"))) # 计算两点距离(Haversine公式)和瞬时速度 df = df.withColumn("distance", haversine(col("latitude"), col("longitude"), col("prev_lat"), col("prev_lon"))) df = df.withColumn("time_diff", col("gps_time").cast("long") - col("prev_time").cast("long")) df = df.withColumn("speed_kmh", col("distance") / (col("time_diff") / 3600)) # 过滤掉超过阈值或无法计算的记录 df = df.filter((col("speed_kmh") < 120) | col("speed_kmh").isNull())这段代码如果你在Pandas里写,跑个几百万行就会内存告急,但在Spark分布式环境下,按order_id分区后每个分区独立计算,几千万行也就几分钟的事。
3.4 Hive SQL:清洗规则下沉到数仓
除了Spark和MapReduce,Hive SQL也是大数据清洗的重头戏。数据仓库里大量的ETL工作,其实就是用Hive SQL写清洗逻辑。核心思路是把清洗规则变成一条条SQL,定期跑成结果表。
拿招聘数据的薪资字段举例,原始数据是“8k-15k”“面议”“10万/年”这种字符串。你可以用split函数拆出上下限,再映射成统一的数值字段:
SELECT job_id, CASE WHEN salary_range LIKE '%k%' THEN CAST(SPLIT(REPLACE(salary_range, 'k', ''), '-')[0] AS DOUBLE) * 1000 WHEN salary_range LIKE '%万/年%' THEN CAST(SUBSTRING(salary_range, 1, LOCATE('万', salary_range) - 1) AS DOUBLE) * 10000 / 12 ELSE NULL END AS salary_min_monthly, ... FROM raw_recruit_data;Hive SQL的好处是它同时适用于离线批处理和即席查询,而且语法对后端、数据仓库工程师来说门槛低,不用写Java或者Scala。所以我建议学习大数据清洗时,SQL和Spark两条腿走路,缺一不可。
4. 一套可复用的数据质量检查清单
说实话,我前几年做清洗也是“一把梭”,拿到数据先dropna,看到重复就drop_duplicates,完全没有章法。后来有一次交付给业务方的报表被质疑数据有问题,我复盘才发现,罪魁祸首竟然是我把“业务上合法的空值”当成脏数据删掉了。从那以后,我开始建立一套数据质量检查清单,每个项目先过一遍清单再动手清洗。
4.1 七维度检查项
我把数据质量问题归纳为七个维度。你可以把这张表打印出来,接到一张新表时逐个过,保证不遗漏:
| 维度 | 检查问题 | 常用方法 |
|---|---|---|
| 完整性 | 缺失值占比多少?缺失是否有业务含义? | isnull().sum() / df.info() |
| 唯一性 | 主键是否重复?重复记录如何处理? | duplicated().sum() / groupby计数 |
| 时效性 | 日期字段是否在合理时间范围内?有无未来数据或过期数据? | min/max时间比较 / 和时间窗口对比 |
| 合法性 | 枚举字段是否有定义外的取值? | value_counts() 对比字典表 |
| 一致性 | 同一实体在不同表中的ID、名称、单位是否统一? | 多表join核对 / 单位换算验证 |
| 准确性 | 数值字段是否落在业务合理区间?有无极端离群值? | describe() / quantile() / 标准差截断 |
| 及时性 | 数据是否按预期时间更新?有无延迟、漏跑? | 最大时间戳对比调度周期 |
这里头“一致性”是我认为最难查的一项,因为它藏在多张表的关联关系里,单看一张表发现不了。比如用户表里user_id是数字格式,但行为日志表里是带“U_”前缀的字符串,两表直接join就什么都匹配不上。通常排查这种问题,要先对两张表分别做取值分布统计,把ID前缀、类型、bit数都拉出来看,才能发现字段口径不一致。
4.2 数据质量报告怎么写
清洗不是洗完就跑,而是要留下一份“数据质量报告”,说明数据现状、问题占比、处理规则。这份报告的价值在于,它能让业务方确认你的清洗口径是否合理,也能让半年后的你自己(或同事)看懂当初为什么要这样处理。
我习惯的格式是:
- 数据集基本信息:来源、时间范围、行数、字段数。
- 问题统计:每类问题检出多少条、占比多少。比如“日期格式异常2135条,占比0.3%”。
- 处理规则:逐类说明怎么处理的,比如“日期格式异常统一为yyyy-MM-dd,无法解析的直接置空”。
- 清洗前后对比:口径、行数、关键字段分布是否合理。
看起来简单,但实际项目中,这份报告往往比清洗代码本身还重要。因为业务方不会看你的代码逻辑,但会看你的清洗规则是否欺负了他们的数据。
4.3 从手动清洗到自动化流水线
数据质量检查如果每次都是手动跑一遍,无异于自欺欺人。成熟的做法是把检查逻辑写成定时任务,每天跑完自动出报告,发现问题主动告警。
这里给一个轻量级的自动化思路,适合个人或小团队:
- 用Airflow或简单的cron调度,每天定时执行清洗脚本。
- 脚本里写好checkpoint,每一步清洗前先跑Count校验,行数突变立刻停止。
- 清洗完成后,生成数据质量报告存到指定目录,并统计关键字段的空值率、重复率指标。
- 指标超过阈值时,往钉钉或邮件机器人推一条告警。
这套体系听起来高大上,其实代码量不大,核心价值在于把清洗这件事从“一次性手工活”变成了“可监控的常态化流程”。这也是求职时展示职业素养的一个亮点。
5. 坑位复盘:肉眼看不见的脏数据怎么揪出来
做数据清洗做得越久,你就会发现,脏数据里最阴的不是那些一眼能看到的NULL,而是“看起来完全正常,但一算就错”的数据。这一节我把我实际踩过的坑拿出来复盘,每个都是血泪教训,希望能帮你少走弯路。
5.1 单位不一致的坑
有次我处理一家电商的成交金额数据,拿到手发现销售额字段的值从“0.5”到“1500000”全都有,起初我以为是正常的订单大小差异。直到我写了一个按月汇总的报表,发现某个月销售额骤降90%,业务方说“不对,我们大促明明做了”。
排查下来才知道,这家公司有两个业务系统,一个系统金额单位是“元”,另一个系统是“万元”,两张表合并时没有做单位转换。1.5万元在“元”字段里显示成“15000”,但在“万元”字段里显示成“1.5”,两者直接相加,数据完全错乱。
这个坑的教训是:拿到数值型字段,第一步永远不是算均值,而是看分布。用describe()看count、min、25%、50%、max,如果min和max的位数差异巨大,或者数值分布出现明显的“断崖”,就要怀疑是不是单位不统一。
5.2 时间字段的格式浩劫
时间字段是我认为脏数据“重灾区中的重灾区”。同一个订单表里,可能出现“2024-05-01 10:00:00”“20240501100000”“2024/05/01”“05-01-2024”四种格式,甚至还有只有日期没有时间的情况。
更坑的是,有些人会用Excel的序列号来存时间,比如“45234”表示某天,你在Excel里看到的是日期,导出成CSV就变成了一串数字。这种数据进到Pandas里,如果你不识别,它就是个普通的int,后续做时间排序、时间差计算全部错位。
我现在的习惯是,任何时间字段在接入后第一件事就是统一转成时间戳,然后检查min和max是否落在业务预期范围内。如果发现大量解析失败,直接用errors="coerce"置空,再统计置空比例,比例高就说明源系统的时间导出逻辑本身有问题,得回去找源头。
5.3 空值其实有好多面孔
刚入行的朋友以为空值就是np.nan或者None,实际业务数据里空值的表现形式五花八门:空字符串“”、字符串“NULL”、字符串“null”、字符串“\N”、全角空格,甚至还有“-”和“unknown”这种语义化占位符。
如果你直接写df.dropna(),以上这些“假空值”一个都删不掉。我踩过的一个具体例子是:统计用户手机号覆盖率时,明明看起来缺失率只有1%,但仔细一看,缺失的手机号全被填成了“0”或“00000000000”,所以之前研究表的人都以为手机号是全的。
正确的做法是:先把所有假空值统一替换成真正的np.nan,再统计缺失率:
import numpy as np replace_values = ["", "NULL", "null", "\\N", "-", "NA", "unknown", "00000000000"] df["mobile"] = df["mobile"].replace(replace_values, np.nan) print(df["mobile"].isnull().mean())这一步看似简单,但漏掉任何一张表,后面的分析结果都可能被带偏。
5.4 脱敏字段混入业务数据
热词里有“隐私大数据清洗工具”,刚好说到了隐私数据处理。做数据清洗时,经常要面对手机号、身份证号、邮箱等敏感字段。合规的做法是脱敏而不是删除,毕竟很多分析还需要用到这些字段的部分信息。
我在项目里常用的脱敏规则有三种:
- 手机号保留前3位和后4位,中间4位用星号代替。
- 身份证保留前4位和后4位,中间部分打码。
- 邮箱保留用户名前3个字符加星号,域名保留不动。
代码很简单:
import re def mask_phone(phone): return re.sub(r'(\d{3})\d{4}(\d{4})', r'\1****\2', str(phone)) def mask_email(email): parts = email.split("@") return parts[0][:3] + "***@" + parts[1]但这里有个特别要注意的坑:脱敏后千万别把“****”当成真实值去参与计算,比如有人用手机号长度做校验,脱敏后长度不变还好,但如果你用的是“保留前3后4”的规则,手机号从11位变成了8位,参与校验就会误判。所以脱敏规则一定要和后续使用场景配套,脱敏字段单独存放,业务计算字段另立一列或直接删除原始值。
5.5 连接操作把行数搞爆炸
最后一个坑,我觉得最能体现数据清洗和数据分析的结合点:join 之后行数激增。很多人在做多表关联时,想当然地认为“只要我按user_id join,行数就等于主表的行数”。但实际只要关联字段在另一张表里不是唯一的,join结果就会变成笛卡尔积,行数翻倍甚至翻百倍。
我排查这种问题的经验是:join之前先分组检查关联字段在副表里是不是主键。手法是:
dup = df_right.groupby("user_id").size().sort_values(ascending=False) print(dup.head(10))如果发现最大值远大于1,说明副表里有重复的user_id,这时候要么做去重去业务上取一条,要么明确这个关联本身是“一对多”的关系,再决定join口径。这一步不做,洗得再干净的表,一旦join一塌,前面的功夫全白费。
6. 新手路线图和作品集建议
最后一部分,我想专门说说怎么学习数据清洗、怎么做毕业设计,以及怎么把清洗能力转化成可展示的作品,毕竟这也是“开启数据新征程”最实在的一步。
6.1 合理的学习顺序
我建议的学习路线是分三个阶段,而不是一上来就全部工具都整一遍:
- SQL + Excel:先把SQL的select、where、group by、join这些搞熟。数据清洗一半以上的工作其实就是查问题、看分布,SQL是成本最低的手段。Excel也别嫌弃,它帮你建立“数据感”。
- Python + Pandas:当你需要处理批量文件、复杂的字符串清洗、自动化的脚本时,Pandas是绕不开的。重点学读入导出、空值处理、类型转换、去重、合并、apply/lambda。
- Spark/Hive:数据量到了分布式级别,才需要学Spark和Hive。这时你不是在学“另一个Pandas”,而是在学习分区、shuffle、窗口函数、广播变量这些分布式概念。Hive SQL是必选项,因为离线数仓里它是主力。
每个阶段都找一个真实数据集练手,我最推荐练习的就是网约车轨迹、电商订单、招聘数据这三种,因为脏数据种类丰富、业务逻辑清晰、面试聊起来也有话讲。
6.2 毕业设计和竞赛怎么把清洗做成亮点
很多人的毕业设计喜欢把重点放在算法模型上,但数据清洗其实更容易做出差异化亮点。比如你做课程设计或者参加MathorCup这类大数据挑战赛,评委会看你的完整链路,如果你能拿出一份数据质量报告、一套完整清洗规则、以及清洗前后模型效果的对比,这比单纯调参跑分要有说服力得多。
具体操作上,我建议在毕设和技术博客里保留一个专门小节,明确写出:
- 你拿到原始数据后,发现了哪几类问题。
- 每类问题你采用了什么策略(比如缺失值用中位数填充、异常值置空或剔除、单位不一致做了归一化)。
- 清洗后数据分布的变化,以及同一分析任务在清洗前后结论有什么不同。
这三段写下来,你的作品集和答辩说服力会提升一个档次。尤其是“清洗前后结论不同”这一点,最容易让评委觉得你真的懂了数据,而不是只会套模板。
6.3 建立自己的脏数据标本库
最后分享一个我在实操中很受用的习惯:建一个自己的“脏数据标本库”。每次在项目里遇到新的脏数据形态,我都会把脱敏后的样例、问题描述、清洗脚本、坑位教训记到一个文档里。时间久了,这本“标本库”就是一份非常值钱的实战手册。
我现在的标本库里已经积累了三十多种脏数据形态,包括“负数金额”“金额带货币符号”“同义词混用”“日期跑到了未来”“经纬度反了”“id有空格”“emoji混入文本”等等。遇到新项目时,直接对照标本库逐项检查,效率和正确率都高得多。
学数据清洗这件事,没有太多捷径,就是多碰脏数据、多复盘、多沉淀。你手上烂数据见过得越多,处理得越熟练,你在数据领域的底子就越扎实。记住,模型可以现学,框架可以现装,但面对一张乱糟糟的原始表时那种“我知道问题在哪、怎么处理、为什么这么处理”的判断力,才是真正能拉开差距的本事。