数据加载与预处理:用 cuDF 搞定 CSV 和 Parquet
好,咱们进入实战环节的第一步——把数据弄进来,再整理的干干净净。
说实话,我在早期教 GPU 加速课程时,发现不少同学在 cuDF 的 IO 上就卡住了。不是读不进来,就是读进来之后类型全乱了。其实说白了,cuDF 的 API 跟 Pandas 几乎一模一样,但底下走的完全是另一套路。
这一节,我带你亲手把 CSV 和 Parquet 喂给 GPU,顺便处理那些烦人的缺失值和数据类型问题。
4.1 cuDF 读取 CSV 文件
我们先从最常见的 CSV 开始。cuDF 的 read_csv 和 Pandas 的接口长得非常像,但参数上有些关键差异。
import cudf
import pandas as pd
# 读取 CSV
gdf = cudf.read_csv('user_behavior.csv',
dtype={
'user_id': 'int64',
'age': 'float32',
'income': 'float64'
},
na_values=['NA', 'NULL', ''],
usecols=['user_id', 'age', 'income', 'city'])
print(gdf.dtypes)
这里我用了 na_values 参数。为什么?因为之前我在一个电商数据集里,缺失值被写成 'NA' 和 'NULL' 混着来,cuDF 默认只认空字符串。那次排查了我半小时...
还有一个坑:不要用 CSV 的 header=None 然后自己拼列名。cuDF 的分区读取机制在 header 处理上有时会打架,我建议直接让 CSV 自带表头,或者用 names 参数指定并设置 header=0。
low_memory=False。这个参数在 cuDF 里会关闭分块读取,直接吃满显存。我之前有个学员,一个 20GB 的 CSV 就这么读,结果把 T4 的 16GB 显存干爆了。
4.2 读取 Parquet:GPU 的最爱
我个人强烈推荐:只要可能,请用 Parquet。
Parquet 是列式存储,天生为 GPU 的并行处理设计的。cuDF 读 Parquet 比 Pandas 快 10-20 倍,这不算夸张。
# 读取单个 Parquet 文件
gdf_pq = cudf.read_parquet('features.parquet')
# 读取目录下的所有 Parquet(相当于一个数据集)
gdf_pq_dir = cudf.read_parquet('data/part-*.parquet')
你看看,代码就这么几行。但我要提醒你一个经验:Parquet 的分区列。
如果你的 Parquet 是按日期分区的(比如 data/date=2025-01-01/),cuDF 默认不会自动把分区目录名解析成列。你需要显式设置:
gdf = cudf.read_parquet('data/date=*/',
columns=['user_id', 'score'],
filters=[('date', '>=', '2025-01-01')])
这样只读需要的列和分区,内存效率极高。
| 格式 | 读取速度 (1GB 测试) | 显存占用 | 推荐场景 |
|---|---|---|---|
| CSV | ~500 MB/s | 高(需要整个解析) | 临时数据、小数据集 |
| Parquet | ~4 GB/s | 低(列裁剪+压缩) | 生产环境、大数据集 |
速度差异为什么这么大?CSV 需要逐行解析、推断类型,这些在 CPU 上并行效率低。Parquet 自带 schema 和压缩,GPU 可以直接在列上并行解压——你想想看,一个 SM 处理一列,几十个 SM 同时开工,能不快吗?
4.3 数据类型:精准控制,别让 CUDA 报错
GPU 对数据类型极其敏感。我见过最多的报错就是 CUDA_ERROR_ILLEGAL_ADDRESS,原因往往是类型不匹配。
cuDF 支持的类型比 Pandas 多,但也更严格:
- 整数:int8, int16, int32, int64
- 浮点:float32, float64
- 布尔:bool
- 字符串:object (本质上是 GPU 上的列式字符串)
- 时间:datetime64[ns]
- 可空类型:int32? 表示可空整数(这个后面细说)
判断类型和转换类型:
# 检查类型
print(gdf['income'].dtype) # float64
# 转换(注意:这是原地操作,返回新 DataFrame)
gdf['income'] = gdf['income'].astype('float32')
# 更高效的方式,如果你要转换多个列
gdf = gdf.astype({'income': 'float32', 'age': 'int32'})
嗯,这里要注意一点:不要用 pd.to_numeric 的等价写法。cuDF 没有直接的 to_numeric,你得用 .str.to_float() 或 .astype 配合错误处理。我之前试过直接用 astype 把包含 'NA' 的列转成 float,直接崩了……必须先用 na_values 或者 fillna。
.str.replace() 清理,再转数值。比如:gdf['amount'] = gdf['amount'].str.replace(',', '').astype('float32')。这招在落地时非常实用。
4.4 缺失值处理:GPU 上的特殊玩法
在 GPU 上,缺失值的处理方式和 CPU 有本质区别。Pandas 里,缺失值用 NaN 或 None 表示。CuDF 里呢?
- 数值列:用
(可空类型)或者NaN(浮点)。 - 整数列:默认不能有
NaN。如果你想在整数列里表示缺失,必须用可空类型,比如int32?或Int32(注意大小写)。
先看看数据里有没有缺失值:
# 快速检查
print(gdf.isnull().sum())
# 如果你发现某列有很多缺失,比如 'age' 有 100 个 NaN
# 直接填充
gdf['age'] = gdf['age'].fillna(0)
填充的方法跟 Pandas 一样:fillna()、ffill()、bfill()。但!有一个性能陷阱:千万不要对整个 DataFrame 用 fillna(method='ffill')。这个操作在 GPU 上需要做一次全局同步,非常慢。
# 推荐:对单列操作
gdf['price'] = gdf['price'].ffill() # 没问题
# 不推荐
gdf = gdf.ffill() # 慢,因为要协调所有列
isnull().sum() 扫一遍,如果缺失值比例小于 5%,直接 dropna() 扔掉。如果超过 20%,我会根据业务逻辑填充。GPU 上的 dropna 其实很高效,因为它只需要筛选出有效的行,不用改数据。
删除缺失行:
# 删除任何包含缺失值的行
gdf_clean = gdf.dropna()
# 删除指定列中有缺失的行
gdf_clean = gdf.dropna(subset=['user_id', 'income'])
# 删除所有列都缺失的行(极少用)
gdf_clean = gdf.dropna(how='all')
你可能会问:为什么 cuDF 的 dropna 比 Pandas 快那么多?因为 GPU 可以并行地、按列地检查每个值是否为 null,然后生成一个布尔掩码,一次性地把有效行拷贝到新内存里。CPU 上则要逐行遍历,串行地读。
最后,把清理干净的数据保存起来——当然,用 Parquet:
gdf_clean.to_parquet('clean_data.parquet', index=False)
这样你的数据就已经在 GPU 上,准备好了下一步的降维操作。