欢迎光临
我们一直在努力

Python 处理 100行×75万列超宽数据:字符串转数值的6种策略实测对比

Python 处理 100行×75万列超宽数据:字符串转数值的6种策略实测对比

标签:Python 数据分析 性能优化 pandas numpy polars

一句话结论:字符串→数值转换只做一次,Polars 在大规模下完胜,结果缓存为 Parquet(float32),后续读取快 8~700 倍。


请添加图片描述

一、问题背景

在测试/测量领域,一次全面测试可能产生 75万个测试项 的测量数据。100次采样就是 100行 × 75万列 的超宽表。更棘手的是——仪器导出的原始数据是 字符串格式,必须转成数值才能计算、图等参数。

这个数据形状非常极端:

指标值
行列数 100 行 × 750,000 列
总单元格 75,000,000(7500万)
CSV文件大小 577.9 MB
字符串内存(实测) 1065 MB
float64 内存 572 MB
float32 内存 286 MB

直接用 pandas 的 read_csv + 逐列 to_numeric 转换,在真实 75 万列规模下 10 分钟都无法完成。这不是 pandas 不努力,而是这种超宽+字符串的场景已经远远超出了它的舒适区。

本文用真实数据实测 6 种策略,给出耗时、内存、代码和踩坑记录。


二、测试环境

项目配置
系统 macOS (Apple Silicon)
Python 3.13.12
pandas 2.x
polars 1.x
numpy 2.x
pyarrow 17+
数据文件 raw_100x750000.csv,577.9 MB

三、6种策略实测

策略 S0:pandas 逐列转换(基准)

最直觉的写法:

df = pd.read_csv(csv_path, dtype=str) # 全部读为字符串
for col in df.columns: # 75万列循环
df[col] = pd.to_numeric(df[col]) # 逐列转换

实测结果:超时(10 分钟未完成)

原因:

  • dtype=str 时 7500 万单元格各持一个 Python str 对象
  • for col in df.columns 循环 75 万次,pandas 每列调度开销巨大
  • 字符串 + float 同时存在,内存峰值不可控
  • 请添加图片描述


    策略 S1:分块读取 + 批量转换 + Parquet 缓存

    思路:每次只读 5000 列,转完释放,降低内存峰值。

    all_cols = pd.read_csv(csv_path, nrows=0).columns.tolist()
    chunk_size = 5000
    chunks = []
    for start in range(0, len(all_cols), chunk_size):
    col_subset = all_cols[start:start+chunk_size]
    df_chunk = pd.read_csv(csv_path, usecols=col_subset, dtype=str)
    arr_float = df_chunk.to_numpy(dtype=str).astype(np.float32)
    chunks.append(pd.DataFrame(arr_float, columns=col_subset))
    del df_chunk
    gc.collect()
    df_final = pd.concat(chunks, axis=1)
    df_final.to_parquet("converted.parquet")

    实测结果:超时(10 分钟未完成)

    原因很意外:每次 pd.read_csv(usecols=…) 都要 重新扫描整个 CSV 文件。75 万列 / 5000 列 = 150 次全文件读取 ≈ 86 GB 的 I/O。分块省内存的代价是 I/O 爆炸。

    教训:对超宽 CSV 做列分块,如果每次都要重读文件,不如一次性读完。


    策略 S2:Polars 一行搞定(转换阶段最佳)

    import polars as pl

    cols = pl.read_csv(csv_path, n_rows=0).columns
    schema = {c: pl.Utf8 for c in cols}

    df = (
    pl.read_csv(csv_path, schema_overrides=schema)
    .with_columns(pl.all().cast(pl.Float32))
    )
    df.write_parquet("data.parquet")

    实测结果:105.4 秒,峰值堆内存 116 MB

    为什么 Polars 在大规模下反超 NumPy?

    • 字符串用 Arrow 连续内存存储,不是每个单元格一个 Python 对象
    • 类型转换用 Rust 表达式引擎并行执行
    • 7500 万个字符串在 Polars 里只占用 Arrow buffer,内存极低

    这是 75 万列真实数据下最快的转换方案。

    请添加图片描述


    策略 S3:NumPy 直接操作(小规模无敌,大规模翻车)

    with open(csv_path, 'r') as f:
    f.readline() # 跳过表头
    lines = f.readlines()

    str_arr = np.array([line.strip().split(',') for line in lines], dtype=str)
    float_arr = str_arr.astype(np.float32)
    np.save("data.npy", float_arr)

    实测结果:196.7 秒,峰值堆内存 6646 MB(6.4 GB)

    在 1 万列的演示测试中,NumPy 是最快的。但到了 75 万列,[line.strip().split(',') for line in lines] 会创建 7500 万个 Python 字符串对象,光是对象头就占数 GB 内存。内存暴增拖累了速度。

    请添加图片描述


    策略 S4-S6:转换后的读取方式

    转换完成后,数据已缓存为 Parquet(float32) 或 .npy。日常读取有三种选择:

    # S4: 全量读 Parquet
    df = pd.read_parquet("data.parquet")

    # S5: 只读需要的列(超宽表杀手锏)
    sample_cols = all_cols[::7500][:100]
    df = pd.read_parquet("data.parquet", columns=sample_cols)

    # S6: memmap 零内存加载
    arr = np.load("data.npy", mmap_mode="r")
    col_means = arr.mean(axis=0)

    读取方式耗时堆内存说明
    S4 全量读 Parquet 48.6s 363 MB 需要全部列
    S5 列子集读 Parquet 6.3s 225 MB 只读 100 列,快 7.8 倍
    S6 memmap 加载 0.105s 295 MB 数据在磁盘,快 460 倍

    请添加图片描述


    策略 S7:数据类型内存对比

    实测 75 万列同一份数据的内存占用:

    类型内存相对倍率
    字符串(object) 1065 MB 3.7x
    float64 572 MB 2.0x
    float32 286 MB 1.0x

    字符串 → float32 减少 73% 内存。测量数据(dB 值、)float32 精度完全够用。

    请添加图片描述


    四、结果汇总

    策略耗时堆内存状态结论
    S0 pandas 逐列转换 >600s 超时 ❌ 75 万列不可行
    S1 pandas 分块读取 >600s 超时 ❌ I/O 重复读取致命
    S2 Polars 一行转换 105.4s 116 MB 转换阶段最佳
    S3 NumPy 批量转换 196.7s 6646 MB 内存代价高
    S4 Parquet 全量读 48.6s 363 MB 日常使用
    S5 Parquet 列子集读 6.3s 225 MB 只读需要列
    S6 memmap 零内存 0.105s 295 MB 最快加载

    五、推荐方案

    ┌─────────────────────────────────────────────────────────────┐
    │ 推荐处理流程 │
    │ │
    │ CSV(字符串) │
    │ │ │
    │ ▼ │
    │ ┌──────────────┐ 首次转换(只做一次) │
    │ │ Polars cast │ ← 75万列下最快,内存最低 │
    │ │ Float32 │ │
    │ └──────┬───────┘ │
    │ │ │
    │ ▼ │
    │ ┌──────────────┐ │
    │ │ Parquet(f32) │ ← 持久化缓存 │
    │ └──────┬───────┘ │
    │ │ │
    │ ┌────┼────┬────────────┐ │
    │ ▼ ▼ ▼ ▼ │
    │ 全量 列子集 memmap 统计计算 │
    │ 读取 读取 零内存 numpy向量化 │
    └─────────────────────────────────────────────────────────────┘

    场景推荐方案优先级
    首次 CSV → 数值转换 Polars cast(Float32) ⭐⭐⭐⭐⭐
    转换后持久化 Parquet(float32) ⭐⭐⭐⭐⭐
    日常读取使用 read_parquet(columns=[…]) ⭐⭐⭐⭐⭐
    内存不足时 numpy memmap .npy ⭐⭐⭐⭐
    纯数值计算 numpy ndarray ⭐⭐⭐⭐
    必须使用 pandas API 先转 Parquet,再读 pandas ⭐⭐⭐

    核心原则:转换只做一次 → 缓存 Parquet(float32) → 后续零转换


    六、踩坑记录

    坑1:pandas read_csv 默认类型推断

    不指定 dtype=str 时,pandas 会对 75 万列逐列推断类型,耗时远超想象。

    坑2:分块读取 CSV 导致 I/O 爆炸

    如果分块是每次 read_csv(usecols=…) 重新读文件,I/O 会按分块数翻倍。75 万列分 5000 列一块,就是 150 次全文件扫描。

    坑3:NumPy 在小规模和大规模表现相反

    1 万列时 NumPy 最快,75 万列时反而被 Polars 反超,原因是 Python str 对象的内存爆炸。

    坑4:float64 浪费内存

    测量数据通常 0.01 dB 精度足够,默认用 float32,内存减半。

    坑5:Parquet 列子集读取的列名问题

    不要用 pd.read_parquet(parquet_path, columns=[]) 去读列名。正确做法:

    import pyarrow.parquet as pq
    all_cols = pq.read_schema(parquet_path).names

    七、总结

    维度最优策略原因
    转换速度 Polars 105.4s,Arrow 后端内存效率碾压
    转换内存 Polars 仅 116 MB,比 NumPy 低 57 倍
    读取速度 memmap 0.1s 加载 75 万列
    实际场景 Parquet 列子集 只读需要的列,跳过其余 74.99 万列
    内存不足 memmap + float32 数据在磁盘,堆内存≈0

    一行结论:对于 100 行×75 万列的字符串型超宽数据,用 Polars 一次性转 float32 → 存 Parquet → 后续按需列子集读取,是工程上最平衡、最省心的方案。如果纯追求读取速度或内存最小化,就用 numpy memmap。


    如果觉得有用,点赞收藏关注三连~

    专注于//数据处理与工程优化

    赞(0)
    未经允许不得转载:171主机测评 » Python 处理 100行×75万列超宽数据:字符串转数值的6种策略实测对比
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址