3. 数据加载与基础处理(cuDF DataFrame)

cuDF 说白了就是 GPU 版的 pandas。API 很接近,但底层跑在显存上。我刚开始用的时候,还总觉得它跟 pandas 一模一样,结果某天直接把显存撑爆了…… 嗯,从那以后我就老老实实先看数据类型了。

这一节,我们聊聊怎么把数据塞进 cuDF,并做最基础的处理。后续所有 cuML 算法,都依赖这里清洗干净的 DataFrame。

3.1 加载数据——read_csv / read_parquet

cuDF 支持 CSV、Parquet、ORC、JSON 等格式。我个人习惯,如果原始文件是 Parquet,优先用 read_parquet,因为列式存储加上 GPU 并行,加载速度比 CSV 快 5~10 倍。CSV 的话,记得指定数据类型,否则默认推断会吃很多显存。

import cudf

# 加载 CSV(推荐先指定 dtypes)
df = cudf.read_csv('data.csv',
                   dtype={'feature1': 'float32',
                          'feature2': 'int32'})

# 加载 Parquet(更省显存)
df = cudf.read_parquet('data.parquet')
小技巧: 如果 CSV 文件特别大,试试 chunksize 参数,分批读取,避免一次性把显存占满。

3.2 看一眼数据——基础元信息

拿到 DataFrame 后,我通常会先做三件事:看头、看信息、看分布。这和 pandas 完全一致,但速度更快。

df.head(10)         # 前10行,确认数据样子
df.info()           # 每列的 dtype、非空数、显存占用
df.describe()       # 数值列的统计摘要

你想想看,info() 里会显示显存占用(Memory usage),这是 pandas 没有的。如果那一行数字比其他列大很多,说明该列 dtype 可能太宽了(比如默认成了 float64),需要降到 float32。

3.3 缺失值处理——别让 NaN 偷偷捣乱

cuML 的很多算法不接受缺失值。所以这一步躲不掉。我习惯先看每列的缺失比例:

df.isna().sum() / len(df)

然后根据业务选择策略:

  • 数值列:用 fillna() 填充均值/中位数,或者用 bfill / ffill
  • 类别列:填充众数,或者加一个 “missing” 类别
  • 缺失极少的行:直接 dropna() 扔掉(别超过总样本的 1%)
# 数值列填充均值
df['age'] = df['age'].fillna(df['age'].mean())

# 类别列填充 'unknown'
df['city'] = df['city'].fillna('unknown')

# 某列缺失超过80%,干脆删掉该列
if df['noisy_col'].isna().mean() > 0.8:
    df.drop(columns=['noisy_col'], inplace=True)
注意:fillna() 默认返回新副本,会占用双倍显存。可以用 inplace=True 减少临时开销,或者链式操作时小心显存溢出。

3.4 类型转换——省钱又省显存

GPU 对 float32 / int32 最友好。我曾经在项目中,整个 DataFrame 默认全成了 float64,结果显存占用翻倍,训练慢得像蜗牛。原因是 CSV 里有一列带小数,cuDF 推断为 float64,其他列也被自动提升…… 后来我强制指定了 dtype。

# 整型超过2^31才用int64,否则用int32
df['user_id'] = df['user_id'].astype('int32')

# 浮点用float32,除非精度要求极高
df['score'] = df['score'].astype('float32')

# 类别型压缩为category类型
df['gender'] = df['gender'].astype('category')

转换后记得再用 info() 确认一下显存占用。一般能降到原来的 1/3 甚至更低。

3.5 分组与聚合——为特征工程热身

cuDF 的 groupby + agg 性能非常强悍,利用 GPU 并行,几千万行分组只需要几百毫秒。我经常用它来算统计特征。

# 按城市分组,计算平均收入和方差
grp = df.groupby('city')['income'].agg(['mean', 'std'])

# 多个聚合函数
stats = df.groupby('city').agg(
    income_mean=('income', 'mean'),
    income_std=('income', 'std'),
    count=('user_id', 'count')
)

注意:分组后的结果还是 cuDF DataFrame,可以直接喂给 cuML。但 groupby 这个操作本身不会修改原始数据,放心用。

3.6 一句话流程总览

下面这张图总结了从文件到可建模数据的核心步骤。我刚做 GPU 数据 pipeline 时,就是照着这个顺序走的,几乎没出过岔子。

cuDF 数据加载与预处理流程 加载数据 CSV / Parquet 基础概览 head / info / describe 缺失值处理 fillna / dropna 类型转换 astype 分组统计 groupby + agg 交付 cuML 清理完毕的 DataFrame 顺序执行,也可跳过步骤
关键点总结:
  • 加载时提前指定 dtype 省显存
  • 缺失值必须先处理,否则 cuML 直接报错
  • 类型转换是压缩显存的性价比之王
  • 分组聚合可以原地返回新 DataFrame,不污染原数据

做完上面这几步,你的数据就基本干净了。接下来可以直接扔进 cuML 做特征学习。记住一个原则:把数据整理得越规整,模型训练就越顺畅——我早期项目里一半的 bug 都出在加载和清洗阶段,后来严格按照这个流程走,几乎没再翻车过。