前两篇聊完数据科学到底在解决什么问题、以及数学基础该怎么补之后,这一篇终于进入正题。数据科学圈子里流传最广的一句话是:一个数据项目真正花在建模上的时间可能只占两成,剩下八成全在数据处理上。这句话一点都不夸张,我在好几个项目里都遇到过类似的情况——数据采集很顺利,一到清洗和转换阶段就开始失控,字段对不上、格式五花八门、缺失值一大片、量纲还不统一,最后模型还没开始调参,人先被数据折腾麻了。这篇《数据处理 01》想做的,就是把数据科学里最基础也最容易被低估的这套数据处理打法,从头到尾拆开讲一遍。不管你是刚入行的数据小白,还是已经写了几个月Python但总觉得处理数据时心里没底的半熟手,这篇都适合你——我会把处理的层级、流程中每一步"为什么这么做"、以及工具链怎么选,尽量用玩游戏过关的方式讲清楚。
标题既然叫"游戏科学",那咱们就顺着这个思路来:数据处理不是一个苦力活,它更像一个策略游戏。你手里握着一堆原始数据,就好比刚进新手村,装备乱七八糟,任务目标也不清晰,你得先做任务规划、资源盘点,再一步步把数据打磨成能上战场的形态。这一篇先解决主线任务——把"从原始数据到可用数据"这条路上的关键关卡全部摸清。
1. 数据处理在数据科学中的真实分量:它为什么值得单独开一篇
很多人最开始接触数据科学,往往是被机器学习、深度学习这些名词吸引过来的,觉得建模、调参、出精度才是核心本事。但真正上手做过一个完整项目之后,我的体会恰恰相反:模型的优劣当然影响结果上限,可数据处理的质量直接影响结果下限——数据是脏的、乱的、缺的,再好的模型也救不回来。用一句更直白的话说:模型决定你能跑多快,数据处理决定你能不能上跑道。
我从几个实际项目中感受特别深。一次是处理一批线上商城的历史订单表,看着没多大,几十万行,但里面用户ID有的是数字,有的是带字母的字符串;时间字段一会儿是"2023-07-01 14:23:11",一会儿是"2023/7/1 14:23",还有的直接存成了时间戳;价格字段里居然混进了"99.00元"这种带文本的值;地区字段有37种写法,实际上对应的省份只有15个。这些还只是"字段级"的杂乱,等你要做用户复购分析的时候,才发现同一用户因为ID格式不一致被拆成了好几个人。一个本该两三天跑完的分析,光数据清洗就耗掉了一周。
第二个例子是传感器时间序列数据。设备每秒钟上报一条状态记录,看起来非常有规律,但真实环境里网络抖动、设备重启、服务端宕机都会造成断点。如果你直接拿这份数据去做趋势预测,断点前后数据会被算法误判成"正常连续变化",预测出来的结果自然就偏了。你必须先做时间戳对齐、做插值策略的选择、做异常跳变的识别——这些全都在建模之前发生。
第三个是与热词里那些"SAR数据处理""TEM数据处理""PET数据处理"相关的思路其实是相通的。不管你是处理遥感影像、电镜图像还是医学影像,RAW数据极少可以直接用,都要经过辐射校正、几何校正、噪声滤除、格式转换、分辨率重采样这一套流程。很多做具体领域研究的人以为自己只是在"做预处理",实际上这恰恰是整个研究链条里复用性最强、最容易出成果也最容易踩坑的部分。
所以我把数据处理单独拆出来成一篇文章,而且计划写上几篇,不是因为它高深,而是因为它是数据科学中被低估得最严重的一个环节。数据科学里有一句话我觉得说得很透:Garbage in, garbage out。你把垃圾数据喂给模型,模型只会回你一堆垃圾结论。而在真实业务里,一手的数据从来都不是为你准备好的,它们是为业务流程准备的,你要做的,就是在这套"为业务而生"的数据里,重新建立一套"为分析而生"的数据视图。
再加上现在围绕数据科学的热搜词里,Python科学计算、NumPy、SciPy、Matplotlib、数据预处理、流式数据处理、数据处理框架这些东西被反复提及,说明大家都意识到这一步的重要性,但市面上的资料往往散落在各个工具文档里,很少有人把"处理目标—处理步骤—处理工具—常见陷阱"串成一条完整的链路。这一篇就是想做这个串联工作。
2. 数据处理的三个层级:从"能用"到"好用"再到"有用"
在我自己带项目的时候,习惯把数据处理拆成三个层级来看。这个分层不是教科书上的标准定义,是我从实践中总结出来的,目的很简单——让团队里的新人知道自己的工作处在哪个环节,下一步该往哪走。
第一个层级是"数据能用"。这是最基础的,也是百分之百绕不开的一步。原始数据到了手里,先要解决能不能被程序正常读取的问题,具体包括编码是否可以正确解析、字段名是否统一、数据类型是否正确、取值范围是否在合理空间内、重复记录是否需要去重。这个阶段处理完之后,数据可以送进分析流程,但还不代表分析结果是可信的。就好比你把一堆零件从仓库里搬到了工作台,零件还是那些零件,只是整齐了一点。
第二个层级是"数据好用"。这个层级解决的是数据对算法是否友好。你会发现,原始数据里时间戳要么是字符串要么是时间戳秒数,模型没法直接理解;地区字段是中文文本,送到机器学习模型里前得做编码;年龄和收入两个特征的数值范围差了几百倍,如果直接丢给基于距离计算的算法,年龄这个特征基本就被"碾压"了。这个阶段做的事就是特征变换、标准化、归一化、编码转换、缺失值处理、异常值处理。处理完之后的数据,已经可以直接作为模型输入了,而且不至于让算法因为某些特征尺度过大而产生偏倚。
第三个层级是"数据有用"。这个层级开始带有特征工程的意味,目的是创造新的信息增量。原始数据里的一个时间戳,在"有用"层面可以拆分出年、月、日、星期、是否为节假日、是否为促销期、距离上次购买的天数等衍生特征。文本评论可以拆解成情感得分、词频向量。地理位置信息可以进一步计算距离某个核心网点的空间距离。这一步的本质,是把数据从"记录了什么"推向"这个记录能预示什么"。很多时候模型精度的提升不是靠换更强的算法,而是靠这个环节有没有做出真正有区分度的特征。
三个层级放到一个具体的例子里面会更好理解。假设你手里有一份车辆轨迹数据,GPS每隔五秒上报一个经纬度点,还附带了时间、速度、方向角。如果只做到"能用"层级,你做的事情是把纬经度格式统一、把速度里的异常0值标记出来、把时间字段解析成标准格式。到了"好用"层级,你要处理GPS漂移,剔除那些不合理的跳跃点,平滑速度序列,把方向角度转成弧度。到了"有用"层级,你就开始创建特征了——相邻轨迹点之间的航向偏差是否过大、每个路段上的平均速度与限速的关系、停车点提取、OD聚类、行程切分。同样一份数据,层级不同,分析深度完全不同。
我见过不少新手在一开始非常急,拿到数据就急着跑模型,跳过了前两个层级直接去追求"有用"——这是最容易翻车的路线。没有清洗干净的数据会注入隐藏偏差,没有标准化的特征会让模型权重解释失真,没有做缺失值策略会让样本量忽大忽小,导致每一次跑出来的结果都不一致。所以这一篇里我着重要讲的就是前两个层级,第三个层级会在特征工程的部分再深入展开。
3. 通用数据处理的完整链路:每个环节在做什么、为什么非做不可
脱离具体场景谈数据处理容易变成空话。这一节我带大家走一遍我认为最标准的处理链路,并把我踩过的每一个坑都标出来。这里的顺序不是随便排的,每个环节都有它的上下游逻辑。
3.1 环节一:探索性理解——先读数据字典,再读数据本身
这一步很多新手会跳过,但恰恰是"省时间"的关键。拿到一张表或者一堆文件,先在脑海里建立一个问题清单:每一列的业务含义是什么?取值类型是什么?合法取值范围是多少?有缺失的话缺失比例大概多大?(这些在拿到数据时可以快速用Pandas跑一遍,但这里先不展开,工具篇会说)。我的习惯是先看数据字典,没有数据字典就看字段名和样例数据,再决定后续的清洗策略。
这里有一个非常实用的动作:用describe和info一类的函数快速了解数据的分布形态和空值情况,同时看一眼每列的唯一值数量。唯一值数量如果远小于行数,这一列大概率是类别型;如果几乎等于行数,那大概率是ID或连续型。这个判断会影响你后续选编码方式和缺失值填充策略。
3.2 环节二:字段与编码统一——最枯燥但最救命的一步
第二个环节解决的是"同一个东西叫法不一致"的问题。只要数据源超过一个,这件事几乎必然发生。用户表里叫user_id,订单表里叫uid,日志表里叫memberId,这三个字段是同一个东西,但你如果直接join,程序不会认。再比如性别字段,有的表存1和2,有的表存M和F,有的表存"男""女",有的表甚至存了"未知"。还有编码问题,这在读取CSV文件时特别常见——从某个老系统导出的文件是GBK编码,你用默认UTF-8去读,满屏乱码。
这一环我的固定做法是:先建立字段映射表,把所有表里的同类字段归到一个标准命名下;再统一分类值的编码规范;最后检查文件编码,必要时做编码转换。这里要特别提醒一句——字段映射表一定要留文档,不然三个月后你自己回来看代码,根本想不通当时为什么把A表某个字段设成了这种规则。
3.3 环节三:缺失值处理——先判断"缺失机制",再选策略
这是整个数据处理里最需要动脑子的一环,因为缺失值不是简单的"填上就完事",不同的缺失机制对应完全不同的处理策略。我从统计学角度打个比方:如果数据是"完全随机缺失",比如说用户填问卷时随机漏填了一道题,那你用均值填充是比较安全的;如果是"随机缺失",比如收入高的人更不愿意填收入,那么缺失本身就携带了信息,你需要用一个能够捕捉"是否缺失"这个信息的模型来填补,或者干脆把"是否缺失"单独做成一个特征;如果是"非随机缺失",比如超过设定阈值的那些极端值直接被系统过滤掉了,那你几乎无法从数据内部自行修复,只能回到业务源头去核实。
实操里我的策略优先级是这样的:能删则删的,只限于缺失比例极低(一般低于1%—2%)且该列重要性不高的情形;缺失比例在可控范围内并且是数值型连续变量,用中位数填充比均值更稳健(因为均值对异常值敏感);时间序列数据优先用前后值插值或线性插值而不是全局填充,因为序列局部变化趋势是有意义的;类别变量优先用众数或者单独一个"缺失"类;如果这一列对未来模型很重要而缺失比例又高,千万别硬填——把"是否缺失"本身做成一个特征往往更有效。
3.4 环节四:异常值识别——不是所有"异常"都要删
异常值这一步很有意思,因为它是最需要结合业务背景的一环。单纯从统计学角度看,价格字段出现了一个比均值高几千倍的值,显然是异常;但从业务上看,这可能是公司真实卖出的一个企业级订单,金额就是这么大。所以我的原则是:先识别,再分级,最后才决定处理方式,而不是一见到"异常"就删掉。
常用的识别方法我放在第4节详细讲,这里先给一个思路——不管用什么方法,识别出来之后先把它们单独拉出来看一遍,确认是"录入错误""测量噪声"还是"真实但极端的业务行为"。录入错误改数据;测量噪声平滑处理;真实极端值保留,但后续建模时要考虑是否单独建模或做截尾处理。机器学习中的数据处理里经常提到一个概念叫"健壮性",它的前提就是你在预处理阶段没有武断地把信息抹掉。
3.5 环节五:重复数据处理——别把"去重"想得太简单
重复数据看着简单,真做起来全是细节。首先是"什么是重复"的定义问题。两行数据所有字段完全一致,这是最简单的情况,直接drop掉就可以。但更常见的是部分字段重复——同一用户ID出现了两次,但购买记录不同,这根本不是重复行,这是同一个用户的多次行为。如果同一个用户ID同时存在两条"注册信息"且两条的注册时间不同,你就得判断哪一条是有效的。另一个坑是"时间窗口内的重复"——同一台设备,同一分钟内上报了三条状态相同的数据,这在传感器数据里很常见,属于系统重传导致的冗余,可以通过时间粒度和内容双重判定去做去重。
我的处理顺序是:先按照业务主键识别唯一性,再在此基础上检查完全重复字段,最后考虑时间窗口模糊去重。每一步都要记录删了多少行,原因是什么,方便后面回溯。
3.6 环节六:数据变换与标准化——让不同尺度的特征站到同一平面
这环节是给"数据好用"层级兜底的。两个经典操作是标准化和归一化。标准化是把数据变成均值为0、标准差为1的分布,公式是(x - μ) / σ;归一化是把数据压缩到[0,1]区间,公式是(x - min) / (max - min)。共性在于它们都解决"量纲不同导致算法偏心"的问题。适用场景也不同:如果后续算法假设数据服从正态分布或者涉及梯度下降优化,标准化通常更好;如果后续算法是基于距离计算的但你不希望离群点主导min-max区间,用标准化更稳健;如果只是把特征范围对齐到固定区间,比如图像像素从[0,255]映射到[0,1],归一化就够了。
学科学计算的时候,这个环节就是NumPy和SciPy的主战场——向量化做标准化、快速的百分位计算、通过正态分布检验来选择变换方式,都是基本功。不过这里先卖个关子,细节我会放到第四篇讲Python工具实战的时候挨个演示。
4. 简单有效的数据体检实战:用最小代码快速摸底
理论讲了一堆,很多人可能已经在心里嘀咕:道理我都懂,但拿到一份数据之后,第一步到底应该敲什么代码?这一节我专门给一个可以直接抄作业的"数据体检"流程,用的是Python生态里最主流的三件套——Pandas、NumPy、Matplotlib。学过Python科学计算和数据科学应用的人对这三件套肯定不陌生,它们各司其职:Pandas负责表格操作,NumPy负责数值计算,Matplotlib负责可视化。
先说一个我反复强调的观念:数据体检的核心不是跑出几张统计表,而是对数据形成空间感。所谓空间感,就是你知道每一列大概是什么分布、有没有明显异常、列和列之间有没有可疑关系。我的体检代码通常由四步组成。
第一步是"看一眼":输出前几行数据,确认读取是否正常、字段名是否符合预期、样例数据是不是人话。第二步是"盘一遍":查看每列的数据类型、非空数量、唯一值数量。第三步是"算一笔":对数值列输出描述性统计,重点看min、max、mean、std和几个分位数。这里我有一个经常叮嘱新人的点——不要只看均值,一定要看25%分位数和75%分位数之间的距离,也就是IQR,它能比均值更真实地反映数据的集中趋势。第四步是"画一画":对每一列数值特征做直方图,对可疑列做箱线图,对两两关系做散点矩阵。这一步是Matplotlib的主场,很多人把画图当作出报告时的事,但我的习惯是"先画给自己看再做处理"。一张直方图能在一秒内告诉你这一列是正态、长尾还是双峰,这比任何统计检验都直观。
具体到代码层面,示意大概是这样的:
import pandas as pd import numpy as np import matplotlib.pyplot as plt df = pd.read_csv("raw_data.csv", encoding="utf-8") # 第一步:看一眼 print(df.head()) # 第二步:盘一遍 print(df.info()) print(df.nunique()) # 第三步:算一笔 print(df.describe(percentiles=[0.01, 0.25, 0.5, 0.75, 0.99])) # 第四步:画一画 numeric_cols = df.select_dtypes(include=[np.number]).columns for col in numeric_cols[:6]: df[col].hist(bins=50) plt.title(col) plt.show() df.boxplot(rot=90) plt.show()整个过程跑下来,你对这份数据的"体感"基本就建立起来了,后续做清洗的时候心里有谱。我在实际项目里发现,这四步做完,至少能提前发现一半以上的"数据陷阱":比如某列数值为什么有负数、某列的99%分位数和max差了上百倍、某两列之间出现了完全等比例的同涨同跌关系。这些线索比任何教科书里的"完美数据"都更有分析价值。
如果说上面这些是在局外人视角下的建议,那么再补一句我在真实项目里强调过无数遍的话:不要急着写清洗逻辑,先用这份体检把"数据质量报告"写出来。报告里不需要华丽的词汇,只需要把每一项发现列清楚——哪种字段有多少比例缺失、哪个字段的取值范围异常、哪些字段可能是同一个信息的多种编码形态。这份报告的价值在于,它让你的数据处理变成一个有据可依的过程,而不是"看到哪里改哪里"。
5. 数据处理框架与场景选择:什么时候该用Pandas,什么时候该上Spark
随着项目规模从小到大,我越来越意识到一个问题:很多人在一开始就陷入工具选择的焦虑——文件一大就想着是不是该上大数据框架,数据一复杂就想着是不是该换掉Pandas。其实工具选择的核心逻辑只有一条:你的数据规模和计算复杂度,是否已经超出了单机内存的舒适区。
Pandas能处理的数据量,在多数业务分析场景下完全够用。说个大概的量级感受:单机上处理几百万行、几十列的结构化数据,如果逻辑不复杂,Pandas都能在一分钟到几分钟内完成,内存占用看列数和数据类型,一般几个GB以内都是可以接受的。只有当数据量达到数亿行、数据源分布在多个节点、计算需要分布式并行的时候,才有必要考虑Spark之类的数据处理框架。很多初学者容易陷入"我数据有100GB,是不是必须用Spark"的误区,但实际上100GB的数据,如果其分析只需要某几列,那可以先做列裁剪,把内存占用砍到一个很小的量级,问题就解决了。
另外还有一个场景差异是流式数据。热词里提到了流式数据处理,这跟离线批处理是两类玩法。流式数据处理的特点是数据持续到达、实时计算、窗口聚合,经典工具比如Kafka配合Flink或Spark Streaming。如果你做的是传感器实时日志监控、实时交易风控这类场景,你需要的是流处理思维——对每一个到达的事件做增量状态更新。但绝大多数刚入门的人实际上做的是离线分析,数据是一次性拿到一批的,这时候老老实实把批处理做好,远比硬套流处理框架有价值。我在带新人时经常听到有人说"我们数据是实时产生的,所以要用流式框架",但仔细一问,他们的业务需求其实是每天凌晨跑一次离线报表,这就属于典型的需求和工具错配。
用一张表格把几个常见场景对应一下:
| 场景 | 数据规模 | 典型工具 | 核心关注点 |
|---|---|---|---|
| 小规模结构化表 | 百万行以内 | Pandas / NumPy / SciPy | 清洗、变换、可视化探索 |
| 中大规模结构化表 | 千万至上亿行 | Pandas + 分块处理 / Dask | 内存管理、列裁剪、分块聚合 |
| 分布式大数据 | 数亿行以上 | Spark | 分布式任务调度、数据分区 |
| 流式实时数据 | 持续到达 | Kafka + Flink / Spark Streaming | 窗口计算、状态管理、延迟 |
| 科学计算数组 | 多为数值矩阵 | NumPy / SciPy | 向量化运算、线性代数、信号处理 |
这个表格只是给大家一个初步印象,不是为了让你背下来。真实情况里,Pandas和Spark常常配合使用——先用Spark在大规模数据上做粗粒度的过滤和聚合,把结果收缩到Pandas可以处理的规模,再用Pandas做精细的清洗和特征处理。这种"大框架粗加工、小工具精加工"的组合打法,在工业界非常常见。
再说一个很容易被人忽略的点:工具选择还要考虑团队协作。如果你的数据处理流程要交给别人review,或者要跑在别人的机器上,尽量选择生态成熟、文档齐全的工具。Pandas和Spark之所以能成为主流,不是因为它俩在所有方面都最优,而是因为背后的社区生态足够大,遇到问题你总能在网上找到别人的解决方案。这本身就是一种隐形生产力。
6. 我踩过的高频数据坑:从编码错乱到浮点误差的完整记录
最后这个部分是把我在多个项目里踩过的、并且有一定普适性的坑集中列出来。这些坑不是那种很高深的Bug,恰恰是那些看起来不起眼、却能让结果整体跑偏的细节问题。
第一个坑:文件编码不统一。这个坑我在第3节提到过,这里展开说。当一个数据集来自多个系统时,最常见的组合就是一部分文件是UTF-8编码,一部分是GBK编码。你用Pandas统一用UTF-8去读,GBK文件会报解析错误;就算不报错,某些特殊字符也会变成乱码。我之前在一个项目里就是因为有个字段里的繁体字被错误解编码,导致后面做文本分类时这个字段整列变成了无意义的符号序列。解决办法很笨但很有效:在读取文件之前,先写一个编码检测函数,对每个文件做一次探测;如果你的文件是可以统一转换的,那就全部先转成UTF-8再进后续流程。编码统一这件事属于"不做没事,做了保命"的类型。
第二个坑:浮点数比较。这条看起来像是编程常识,但实际犯错的频率远超想象。当你判断某个数值字段是否等于0、或者某个比率是否达到0.1时,直接用==比较往往会出现意想不到的结果。原因在于浮点数在计算机内部并不是精确的十进制表示,0.1 + 0.2的结果实际上是0.30000000000000004。我的处理原则是:凡是浮点数之间做等值判断,一律用绝对误差或者相对误差来判断;凡是需要高精度计算的金额类字段,尽量用整数类型去存储最小货币单位。
第三个坑:时间字段的"隐藏时区"。时间数据最麻烦的还不是格式五花八门,而是时区不一致。多个源系统可能部署在不同的服务器上,服务器时间的设置又未必统一。如果数据是给国内业务用的,你看到的"2023-07-01 14:23:11"到底是北京时间还是UTC时间,可能你自己都说不清。我建议的处理方式是:在数据处理的一开始就把时间列统一转换成带时区信息的时间戳,再根据后续分析需求转成需要的时区。这个操作一步到位,但很多人往往是做到时间对比对不上的时候才回头补,那时候成本就高了。
第四个坑:批量处理时"一个文件一个样"。这是所有系统性坑里最恶心的一个。你有几千个文件要合并,前十个文件的列顺序完全一致,你写了一个循环,一切顺利;结果跑到第1000个文件,突然发现它的某一列从字符串变成了数字类型,或者列顺序反了。Pandas在读取时会自动推断列的类型,所以同一个业务字段在不同文件里可能被推断成不同数据类型。为了避免这个问题,我在读取多文件时通常显式指定dtype参数,把每个字段的类型固定下来,宁可让程序报错,也不让它"聪明地"自动转换。
第五个坑:没有记录操作日志。数据处理流程一旦复杂起来,你会面临一个非常尴尬的问题:别人问你"这个字段清洗规则是什么、这些行为什么被删掉了",你答不上来。我在早期项目里吃过这个亏,后来形成了自己的规范——每一次数据变换操作之后,都输出一行摘要,记录操作前后行数、列数、受影响字段、以及删改原因。这个习惯看起来多花了几分钟时间,但在数据出问题需要回溯时,能帮你省下几个小时甚至一整天。
这几个坑不是孤立的,它们往往会在同一个项目里连着爆发。比如你先遇到编码问题,处理完编码后又发现类型变了,转完类型又发现时间对不上,一路打怪打到怀疑人生。但也只有经历过这一轮,你对数据处理的敬畏感才会真正建立起来。我自己的体会是,做数据处理时间越久,越不敢在拿到数据的第一时间就写分析代码——先体检、再清洗、留好日志,这套流程虽然看起来慢,但整体的稳定性很高。这也是为什么我会把这一篇定位成"游戏科学"系列里的重要一章:通关的秘诀从来不是操作快,而是每一步都走得稳。后续我把Python科学计算三件套的实操细节、特征工程的完整方法论、以及不同数据类型(时间序列、文本、图像、地理数据)的处理专场一篇篇慢慢填上。