直接用sample()或shuffle处理超大DataFrame会因全量加载导致内存溢出;应采用分块读取+概率采样或流式扫描策略实现低内存随机采样。

为什么不能直接用 sample() 或 shuffle 处理超大 DataFrame?
内存爆掉是常态。Pandas 的 sample() 默认会先加载整个 DataFrame 到内存,再随机选行;df.sample(frac=1) 打乱也得把全部数据载入、重排索引——对几十 GB 的 CSV 或 Parquet 文件,这等于直接触发 MemoryError。更糟的是,如果数据来自数据库或远程文件(比如 S3 上的 100GB Parquet),Pandas 还会试图一次性读完再操作。
用分块读取 + 随机跳过实现低内存采样
核心思路:不全读,只读需要的部分。对 CSV,可用 skiprows + nrows 控制每次读哪几行;对 Parquet,可利用分区或 row group 粗粒度跳过。
- CSV 场景下,用
random.sample(range(total_rows), k)生成目标行号集合,再用pd.read_csv(..., skiprows=lambda x: x not in target_rows, nrows=k)读取——但注意skiprows是函数时,Pandas 仍会逐行判断,IO 不省;更稳的做法是分块读 + 每块按概率采样:pd.read_csv(..., chunksize=100000),每块调用chunk.sample(frac=0.01, replace=False),最后拼接 - Parquet 场景优先用
pyarrow底层 API:用dataset = ds.dataset(...),再通过dataset.scanner(use_threads=True, batch_size=65536)流式扫描,对每个RecordBatch调用batch.to_pandas().sample(...),避免整列加载 - 若只要近似均匀采样(不要求严格随机),可用「系统抽样」:设采样率
p,读第i行时以概率p接收,np.random.random() ——单次遍历、零内存额外开销
打乱超大 DataFrame 的实用替代方案
真要全局随机打乱,又不爆内存,就得放弃“一次到位”。实际中更可行的是分阶段策略:
- 先按某列(如主键或时间戳)排序,再用
pd.read_csv(..., skiprows=..., nrows=...)分段读取,每段内部打乱后写入临时文件;最后用glob.glob("tmp_*.parquet")读所有临时文件并 concat ——虽然不是全局均匀,但比不打乱强得多 - 对 Parquet,用
fastparquet或pyarrow直接写新文件时控制 row group 顺序:先用dataset.to_table().to_pandas()只取小样本估算总行数,再用pa.Table.from_pandas(df).to_parquet(..., row_group_size=100000)写入,row group 本身无序,后续读取时用use_threads=True并配合batch_size随机化加载顺序 - 最稳妥的工程做法:导出到 Spark 或 DuckDB。例如 DuckDB 的
SELECT * FROM 'data.parquet' USING SAMPLE(10)原生支持底层采样,ORDER BY random()也能高效打乱,且内存可控
容易被忽略的陷阱:索引、重复和数据一致性
采样和打乱后,index 很可能错乱或重复,尤其分块处理时没重置索引。这不是小问题——后续 merge、groupby 全会出错。
立即学习“Python免费学习笔记(深入)”;
- 分块采样后必须加
ignore_index=True:pd.concat(chunks, ignore_index=True),否则索引从 0 开始重复 - 打乱后若原数据有业务唯一键(如
order_id),务必验证是否重复:df["order_id"].duplicated().any()——分块读取+随机采样可能无意中引入重复行(特别是没去重原始数据时) - 如果原始数据带分区信息(如
year=2023/month=01),采样时需确认是否要保持分区比例;否则某个月份可能完全消失,导致时间序列分析偏差
真正麻烦的不是怎么写那几行代码,而是采样后你根本不知道它有没有悄悄破坏了数据分布——得靠 df.describe(include="all") 和关键字段的 value_counts() 对比原始统计量。


















