pandas为DataFrame批量赋值衍生列后执行行采样触发MemoryError
报错的根本原因
- 问题根源来自pandas 1.2.3版本的底层BlockManager存储机制:正常一次性生成的多列同类型DataFrame,会把所有同类型列合并为连续的numpy数组块存储,执行带随机整数索引的行采样时,直接从连续块中索引对应行即可,不需要额外分配整列规模的临时内存。
- 你通过循环逐列赋值生成衍生列的操作,会触发pandas的视图优化:
d0['r_%s'%(t)] = d0[0]生成的新列并没有独立分配内存,而是和原第0列共享同一个numpy数组的引用,直接把原本连续的整列存储块拆成了细碎的独立块——原本12个原生列属于一个连续存储块,后续30个衍生列因为共享引用,每个都被识别为独立的存储单元。 - 当执行
d0.iloc[id1,:]的随机行采样时,pandas检测到底层存储是分散的碎块而非连续整块,为了避免后续访问时出现视图引用混乱,会优先把所有float64类型的列重新拼接成一整个连续的完整数组(也就是报错中提到的形状为(40, 22117797)的float64数组,算下来刚好是6.59GiB,和报错的内存申请值完全吻合),再从拼接好的连续数组中切出1%的采样行。这个临时全量拼接的步骤,就是明明最终只需要1%规模数据,却要触发超大规模内存申请的核心原因。 - 为什么直接一次性生成42列的随机DataFrame不会报错?因为这种方式创建的DataFrame底层是一整块连续的float64数组,执行iloc采样时不需要做全量内存重拼接,直接从连续块中索引行内容就可以完成操作,根本不会触发额外的6.59GiB临时内存分配。
可行的解决方案
- 采样前强制重拼连续存储块:执行
d0 = d0.copy(deep=True),这个操作会把所有共享引用的碎块列真正拷贝为独立、连续存储的同类型列,重拼过程只会占用一次全量数据的内存(和当前6GB的基础内存占用持平),后续执行iloc采样就不会触发临时全量数组分配。 - 绕开iloc的连续块检测逻辑:将整数索引采样转换为布尔掩码形式,先把随机采样的行ID转换成布尔标记数组,再通过布尔索引取数:
布尔索引的内存处理逻辑和iloc不同,不会提前做全量列的拼接操作,只会逐列取出对应掩码位置的行数据,全程不会触发6.59GiB的临时数组分配。mask = np.zeros(d0.shape[0], dtype=bool) mask[id1] = True d0s = d0[mask] - 从根源避免碎块视图产生:生成衍生列的时候,强制让新列独立拷贝内存,不要和原列共享引用,比如把循环赋值的语句改成
d0['r_%s'%(t)] = d0[0].to_numpy(),让pandas从一开始就把这些同类型列合并为连续存储块,后续采样自然不会触发临时拼接逻辑。 - 替换采样接口:直接使用
d0s = d0.take(id1, axis=0)完成行采样,take方法会逐列按索引取值,不会触发iloc内部的连续块校验和重拼接流程,内存开销和最终采样结果的规模完全匹配。
内容的提问来源于stack exchange,提问作者Jason
相关产品推荐
相关产品推荐

