|
|
这两年在团队里带人做数据分析,Pandas 是出场率最高的工具。越来越多新同事把它当成“会用就行”的库,但真到上百 MB、上 GB 的数据,清洗慢、内存爆,问题就显形了。下面结合踩过的坑,聊聊我自己更偏工程化的一些清洗与性能优化习惯。
先说数据读取阶段就能省下的一大截。read_csv 时尽量显式声明 dtype 和 parse_dates,尤其是类别型字段用 category,长文本该 object 就 object,数值能用 Int32/Float32 就别上默认的 64 位,省内存是真省。日期解析用 format 定死,别依赖自动推断。大文件建议用 usecols 只取必要列,
还能加上 iterator 分块读取(chunksize)和 compression 参数(gzip、bz2),配合 nrows 先抽样推 dtype,再全量跑,能避免一遍遍 OOM 抽风。
清洗阶段我尽量避免逐行 apply。当你写出一个 lambda 去遍历整列,其实已经输了。优先用矢量化算子、Series.str/Series.dt 家族、map/replace/where 等组合拳。比如字符串标准化:先用 .str.strip().str.lower(),再配一个事先构好的映射表 dict 去统一枚举值,比 if-else
链判断高效太多。数值清洗同理,np.where/Series.clip/Series.fillna 连招,经常能把一屏 for/if 改成三行可读又快的向量化代码。多条件分箱我常用 pd.cut 或 np.select,既直观又省心。
类型与缺失值是第二个坑点。类别列只要枚举稳定,就尽量转成 category,并在 merge/join 前统一 categories,避免隐式 upcast 回 object。缺失值别一把梭 fillna(0),先区分业务含义的缺失(真的未知)和技术缺失(解析失败)。数值列可以用中位数或分组中位数填,时间列用前向/后向填充要加上窗口限制,防止跨主体“串味”。布尔列优先用 pd.BooleanDtype(),能同时表达 True/False/NA,比 object/int8 更干净。
索引与对齐经常被忽视。大表 join 前先确认连接键的 dtype、大小写、空白是否一致,必要时先 .str.strip().str.lower() 并用 .rename 统一列名。merge 选择 how='inner' 能减少爆炸式膨胀;确实要 left/outer 的,记得在连接前后记录行数,快速发现重复键导致的倍增。排序和 set_index 不是为了“好看”,而是为了加速 groupby/merge_asof/resample——时间序列尤其受益。需要按时间窗口配对时,merge_asof 比笛卡尔
积更稳,注意 key 列必须排序且允许的容差方向要想清楚,避免把未来的数据“穿越”到过去。
groupby 是清洗和聚合的主力,但也是性能黑洞。经验是:先把参与分组的键转为更紧凑的类型(category/Int32),再用 sort=False 避免不必要的排序;能用 transform 就别 merge 回去一大张表;需要多指标时,agg 里少用自定义函数,多用内置如 "mean", "sum", "nunique",自定义函数实在必要也尽量写成基于 NumPy 的向量化实现。还有一个被忽略的小技巧:对高基数分组,先做频数阈值过滤,把长尾归并为 “other”,不仅更快,统计也更稳健。
链式赋值要小心。SettingWithCopy 警告不是烦人精,它在提醒你可能在副本上动刀。两种策略:要么在入口处用 df = df.copy() 明确复制,要么用 .loc 显式定位列与行;复杂多步清洗时,可以把每一步写成纯函数并返回新的 DataFrame,最后用 pipe 串起来,既避免副作用,也便于单测和回溯。
说点 I/O 和落盘。中间结果别老是 to_csv,再读回来类型全丢。更推荐 parquet/feather:列式、带 schema、压缩友好,读写比 CSV 快一大截。跨语言或要给下游 Spark/Arrow 用,优先 parquet。CSV 只当交付给非技术同学时的“通用格式”。
内存与并发方面,Pandas 天生单线程,提升空间在“少占内存、少复制、少 Python 层循环”。具体做法:尽早 downcast(pd.to_numeric(..., downcast="integer/float")),用 astype
到更紧凑的扩展类型(Int8/Int16、Float32、StringDtype、Categorical),避免隐式升格到 object。大规模计算时,尽量在列上就地变换(inplace 不总是生效,但用赋值覆盖同名列能减少额外副本)。需要临时视图时用 df.loc[:, ["col1","col2"]] 取列子集,比 copy 整表更省。真的顶不住内存,可以考虑用 dask.dataframe 做“同 API 的外排计算”,或者把关键聚合迁到数据库 |
|