Pandas HyperFrame:从BlockManager到列式存储的架构革新

发布时间:2026/10/29 3:48:21
Pandas HyperFrame:从BlockManager到列式存储的架构革新 聊起 hyperframe 这个词我最初是在 pandas 的 roadmap 讨论区里看到的。当时第一反应是什么渲染技术或者动画格式结果顺着链接一路点进去才发现这是 pandas 内部存储层的一次“换地基”级改造。简单说过去 DataFrame 的每一列并不是独立存放的而是按相同 dtype 打包成整块二维数组这套机制叫 BlockManager而社区正在讨论的下一代方案是把数据改成“每列独立、可按需切块”的 ChunkedArray 结构把整张表组织成一组细粒度的超帧HyperFrame。每一列都能单独切片、单独改写、直接交给 Arrow效率和扩展性完全不是一回事。这篇文章不打算带你把 pandas 源码从头背到尾而是讲清楚 HyperFrame 为什么会冒出来、BlockManager 到底慢在哪、这套新结构对我们每天写DataFrame的人有没有影响。无论你是做数据清洗的分析师还是维护着基于 pandas 二次开发工具的库作者下面这些内容都值得花十分钟看完。1. 先讲清楚HyperFrame 到底是什么1.1 一个词两种叫法要讲 HyperFrame得先承认这里有一个容易绕晕的点pandas 社区管这套新结构并不只叫一个名字。在我翻到的几份设计讨论里早期的思路叫NSFrame命名强调的是列Columns和标签Index的统一管理后来逐渐改口叫HyperFrame重点落在存储单元从二维 Block 变成一维的超帧列集合。虽然名字在变但背后的目标完全一致把 DataFrame 的数据布局从“按类型打包成块”改成“按列独立管理”。为什么这个改变值得关注因为 DataFrame 今天大量的性能瓶颈、内存翻倍、GC 卡顿根子都在 BlockManager 这套“老房子”上。HyperFrame 不是简单地把代码重构一遍而是从存储协议层面换引擎。1.2 一句话版本如果只记一个结论那可以看这张对比表对比项BlockManager现状HyperFrame提案方向存储单元二维 Block同 dtype 列打包一维 ChunkedArray每列独立管理列间关系同块列互相耦合列之间完全解耦切片实现逐个 Block 切片再拼接每列一个 range 视图零拷贝dtype 转换可能触发整个 Block 复制只影响当前列与 Arrow 交互需要做一次“翻译”内部结构天然一致Copy-on-Write2.0 开始支持可开关原生内置写入才复制简单说BlockManager 是把相同类型的列“捆”在一起HyperFrame 是把它们“松开”各自管理需要合作时再按需组合。1.3 谁应该认真看这篇如果你符合下面任意一条这篇内容都值得细看每天在 pandas 上做筛选、切片、合并经常被内存占用和运行时间折磨你维护的库依赖 pandas 内部结构担心未来版本升级后代码崩掉你在评估是否要把技术栈从 pandas 迁移到 Polars、DuckDB 这类新兴方案你想理解 Copy-on-WriteCoW和 Arrow 后端到底在 DataFrame 中扮演什么角色。对这些人来说HyperFrame 不是遥远的概念而是未来三年 pandas 所有重大版本改进的地基。2. BlockManager 的“历史包袱”问题到底出在哪2.1 一块二维数组撑起的老房子要理解 HyperFrame 为什么存在先得知道现在 pandas 内部是怎么存数据的。每个普通 DataFrame 底层都由一个BlockManager管理它里面不是一个一个的列数组而是一个一个的Block。每个 Block 里放着 dtype 相同的多列数据。举一个最常见的场景import numpy as np import pandas as pd N 1_000_000 df pd.DataFrame({ user_id: np.arange(1, 1 N), score: np.random.rand(N), city: [beijing] * N, })在这份数据里user_id和score都是数值类型会被塞进同一个 int64/float64 混合的二维 Block 中city是字符串单独形成一个对象objectBlock。逻辑上你看到的 DataFrame 是三列物理上底层是“两块二维数组”大概是这种感觉BlockManager ├── Block A (数值块存了 user_id 和 score 两列) │ ├── [1001, 0.87] │ ├── [1002, 0.32] │ └── ... └── Block B (object 块只存 city 一列) ├── [beijing] ├── [shanghai] └── ...BlockManager 设计之初是合理的同 dtype 的列放在一起内存连续向量化计算时 CPU 缓存友好。二十年前这是非常优雅的设计但后来数据规模变大、列式存储和 Parquet 格式逐步成为主流这套方案的短板就越来越明显。2.2 问题一同块列耦合引发内存碎片最让人头疼的是内存碎片问题。比如你在跑一个用户行为分析任务原始 DataFrame 有 2000 万行其中user_id和score在同一个 Block 里。某一步你想把score从 float64 转成 float32 来节省内存这时 BlockManager 要做的是新分配一块 float32 的连续内存把两列数据整体搬过去再择机释放旧内存。问题在于user_id和score明明互不影响却因为待在同一个 Block 里被迫跟着一起复制。数据量一大内存峰值轻松翻倍。如果你对同一个 DataFrame 连续做几轮 dtype 调整碎片和临时对象会把内存搞得一团糟。2.3 问题二切片和列操作不够“爽快”再看切片。df.iloc[100:200]看起来很简单但 BlockManager 的底层操作要遍历所有 Block再对每个 Block 做一次行切片。如果切片后的数据还要继续参与后续数据处理BlockManager 甚至可能因为内存布局问题触发隐式复制。更典型的问题是修改单列 dtype。你只想处理city这一列但因为它是 object Block转成新的字符串 dtype 后BlockManager 需要重新排列 Block 结构。一次列的“小动作”经常会引发一个或多个 Block 的整体重建。我在实际项目里遇到过这种情况几千万行的 DataFrame只改了一列字符串类型内存瞬间多出三四个 GB任务直接崩了。2.4 问题三面对列式生态BlockManager 像个“翻译官”还有一个在大数据场景下非常致命的问题现在几乎所有人都会把 DataFrame 写到 Parquet或者把 Parquet 读成 DataFrame。Parquet 天生是列式存储Arrow 的内存格式也是列式的。但 pandas 用 BlockManager 存储两者结构不匹配每次读写都要做一次“翻译”读 Parquet 时Arrow Table 的每一列是独立的要先转成 BlockManager 按 dtype 合并组块写 Parquet 时BlockManager 又要把组好的块拆开还原成 Arrow 列。这一步转换的成本不光体现在 IO 时间上还意味着你对 Arrow、Parquet、Flight 等生态工具做列式下推优化时pandas 很难直接沾光。而 HyperFrame 的设计初衷之一就是让 pandas 内部结构和 Arrow 对齐省掉这层翻译。3. HyperFrame 的核心设计拆解3.1 每列都是一条 ChunkedArrayHyperFrame 最核心的思路是把存储单元从“二维 Block”变成“一维 ChunkedArray”。什么意思每个列不再跟其他列共享存储块而是自己独立拥有一组数据块。拿上面的例子来说HyperFrame 里大概是这样HyperFrame { user_id: ChunkedArrayint64 (块1: [1001, 1002, ...], 块2: [...], ...) score : ChunkedArrayfloat64(块1: [0.87, 0.32, ...], 块2: [...], ...) city : ChunkedArraystring (块1: [beijing, ...], 块2: [...], ...) }每个ChunkedArray在逻辑上就是一列完整数据但物理上可以由多个更小的连续数组Chunk拼接而成。你读数据时访问的是逻辑长列底层却能分别命中不同块。3.2 为什么要拆成多个 Chunk而不是一整列这是 HyperFrame 容易被忽略的点它没有用“一个大数组”存一列而是允许一列内部再分块。这个设计非常聪明。第一分块之后追加数据不需要重新分配整列内存。原来的列满了新来的数据开一个新块续上就行不会出现“为了加几万行把十亿行全搬一遍”的尴尬局面。第二分块天然适合并行计算。多块数据可以交给多线程甚至多进程分别处理最后合并结果。Polars 和 DuckDB 能跑得那么快很大程度上就是靠这种数据布局带来的并行能力。第三单列局部修改时开销极小。假设city列的一个 chunk 需要做数据处理HyperFrame 只需对这个 chunk 做替换其余 chunk 不受影响。我用 PyArrow 做一个最小概念验证import pyarrow as pa # 一个逻辑列由多个 chunk 拼成 col pa.chunked_array([ pa.array([beijing, shanghai]), pa.array([guangzhou, shenzhen]), ]) print(col) # 4 个元素 print(col.num_chunks) # 2 个块对 pandas 用户来说平时操作df[city]不需要关心底下有几个 chunk但对引擎开发者来说这个结构决定了内存管理和并行调度的方式。3.3 零拷贝切片与 Copy-on-Write 的配合列式结构最大的好处之一是切片便宜。在 HyperFrame 里df.iloc[1000:2000]基本就是给每列记录一个 offset 和 length生成一个视图不需要复制底层数组。只有当你去修改这个视图里的数据时才需要真正复制一份这就是Copy-on-WriteCoW。pandas 2.0 开始支持 CoW但它当时只是为 HyperFrame 铺路的“前置协议”。CoW 的思想一句话就能说清读的时候大家共享同一块内存谁想写谁就把要写的部分复制出来再改不影响其他人。import pandas as pd pd.options.mode.copy_on_write True df pd.DataFrame({a: [1, 2, 3], b: [4, 5, 6]}) df2 df # 原本 df2 是 df 的视图现在只修改 df2 的一列 df2[c] 7 # 开启 CoW 后df 不会受到任何影响 print(df.columns) # Index([a, b], dtypeobject)这种写时复制机制是 HyperFrame 实现零拷贝读、按需复制写的重要基石。没有 CoW 之前pandas 为了安全只能频繁复制有了 CoW才能放心大胆使用共享视图把内存浪费降下来。3.4 横向对比Polars、Dask、Vaex 做了什么聊到这里很多人会想到 Polars。确实Polars 从一开始就走的是 Arrow 列式存储路线HyperFrame 本质上就是让 pandas 向这类现代列式结构看齐。但这不意味着“pandas 在抄 Polars”而是“整个生态都意识到列式存储是当下解决大表性能的通用答案”。方案存储模型并行能力与 pandas 关系pandas BlockManager二维块存储差现在的默认实现pandas HyperFrame列式 ChunkedArray中提案中的下一代实现PolarsArrow 列式存储强独立实现的全新框架Dask分区 pandas DataFrame中通过分布式调度拆任务Vaex内存映射列式存储中专攻超大数据集惰性计算Dask 和 Vaex 在架构上各有取舍但共同点都是“不走 BlockManager 的老路”。HyperFrame 的意义在于pandas 不需要被推翻不需要你去学一套新 API而是在保留原有接口的同时把存储层替换成同等现代的结构。这对存量项目实在太重要了。4. 在 pandas 里落地现状、路径与代码演示4.1 提案在讨论中但方向已经很明确先把话说清楚现在打开你本地的 pandas底层用的仍然是 BlockManager。HyperFrame 目前是 pandas 社区正在推进的重构方向对应的是多个设计文档和公开讨论不是已经默认开启的功能。pandas 2.x 系列做的很多工作比如 CoW、Arrow 字符串 dtype、字符串存储后端的切换都是在为这个最终目标铺路。所以别指望明天更新一个版本就能看到df._mgr变成 HyperFrame。你要关注的是趋势官方把列式存储、零拷贝、Arrow 原生支持放在越来越高的优先级上这才是未来。4.2 用 PyArrow 做一次 HyperFrame 概念验证虽然 pandas 还没默认启用 HyperFrame但我们可以用 PyArrow 模拟它提前感受这种存储结构的实用性。import numpy as np import pyarrow as pa N 10_000_000 rng np.random.default_rng(42) # 模拟 HyperFrame 的列式结构每列是一个 ChunkedArray col_user pa.chunked_array([ pa.array(np.arange(1, 1 N), typepa.int64()) ]) col_score pa.chunked_array([ pa.array(rng.random(N), typepa.float64()) ]) # 切片这里不复制数据而是生成一个视图 view_user col_user.slice(1_000_000, 2_000_000) print(view_user) # 你也可以把块的粒度切得更细模拟多 chunk 场景 col_score_chunked pa.chunked_array([ pa.array(rng.random(N // 2)), pa.array(rng.random(N // 2)), ]) print(col_score_chunked.num_chunks)这段代码里slice返回的是视图而非副本这正是 HyperFrame 想要达到的效果。如果 pandas 底层直接使用这种结构df.iloc就能省掉大量隐式复制的开销。4.3 日常使用中你能感知到的变化如果 HyperFrame 全面落地普通 pandas 用户会感知到这几个变化。第一内存占用会明显下降。列与列之间不再相互拖累转换单列 dtype 再也不会导致整块复制。第二切片和筛选速度会显著提升尤其是大数据量下的df.iloc和条件过滤。第三与 Parquet、Arrow 文件的互操作性能会改善读写文件时不再需要 BlockManager 到列式的“翻译”过程。第四行为可预测性更强。CoW 一旦成为默认你就不用再担心df2 df之后改df2会不会“污染”原 DataFrame这是很多人刚学 pandas 时踩过的坑。官方文档会越来越强调“这不是 bug这是特性”。5. 迁移与踩坑生态影响和注意事项5.1 API 兼容大部分代码不用改关于 HyperFrame我听过最多的疑问是“以后我的 pandas 代码是不是要重写”答案是否定的。pandas 官方对这次重构的定位是内部存储替换不是 API 更替。你写的df.groupby(...).agg(...)、df.merge(...)、df.pivot_table(...)这些操作对外接口完全不变。唯一要小心的是那些直接访问私有属性的代码比如df._data、df._mgr、_block等。如果你在项目里写过这类 hack现在就该清理了。5.2 从测试版里得到的几个经验我在测试 CoW 和 Arrow 字符串 dtype 时踩过几个坑提前分享给你。第一个坑开启 CoW 后某些老代码反而会出现更高的内存峰值。原因是不少人以前依赖“隐式修改原对象”这个行为把df2 df当成了低成本复制。开启 CoW 后df2最初只是视图一旦写入触发复制复制发生在写入那一刻如果此时同时保留大体积的原数据峰值依旧会高。解决办法是在明确需要独立副本的地方手动.copy()让复制发生在内存压力较小的时机。第二个坑df.memory_usage()在 BlockManager 下显示的内存并不完全等于真实占用。它按列计算但底层 Block 可能有列共享或额外缓冲。你最好用系统级工具测量真实内存比如 Linux 上跑/usr/bin/time -v python your_script.py或者用tracemalloc跟踪分配趋势。第三个坑Arrow 字符串 dtype 在早期版本里和某些第三方库兼容性一般比如老版本的scikit-learn直接处理 Arrow 字符串列可能会报类型不支持。如果遇到先把列转回普通str或 object dtype 再交给下游。5.3 现在建议做的三件事别等着 HyperFrame 正式落地才动手现在就可以做三件事成本极低收益明确。第一把 pandas 升级到 2.2 以上开启 CoWpd.options.mode.copy_on_write True在 pandas 3.0 里CoW 会成为默认行为。提前开启能让你的代码优先暴露兼容问题。第二字符串存储切换到 Arrow 后端。使用较新的 pandas设置pd.options.future.infer_string True第三检查代码里有没有使用过df._mgr、df._data、BlockManager等私有属性。有的话尽快替换成公开 API或者用df.copy(deepTrue)、df.loc、df.iloc这类标准操作重写。这一步现在做远比未来某天升级后满屏报错再来修复轻松。6. 常见问题速查表我在社区和实际项目中被问过不少关于 HyperFrame 和 pandas 内部机制的问题整理成一张速查表方便你排查。问题原因处理建议df2 df后修改 df2原 df 也会变这是 pandas 旧版的共享引用行为开启 CoW或显式用df.copy()单列 dtype 转换内存突然翻倍BlockManager 同块列被整体复制升级新版 pandas尽量用字符串 arrow dtypedf.memory_usage()与实际内存不一致Block 可能有内部共享/缓存统计不精确用系统级工具或tracemalloc观测真实占用读 Parquet 再写 Parquet速度偏慢需要 BlockManager 和 Arrow 列式之间互转尝试 PyArrow 直接读写再转 pandas某列是字符串第三方库报类型错误Arrow 字符串 dtype 兼容性不足df[col] df[col].astype(str)想知道自己是否用了私有 API代码里出现_mgr、_data、_block用公开 API 重写避免依赖内部结构7. 最后说几句实在话我个人在实际操作中的体会是pandas 这套 BlockManager 的地基确实撑了太久了。二十年前它是聪明设计但今天当一列数据的规模能轻松超过几个 GB、当 Parquet 和 Arrow 已成为行业标准时二维块存储的劣势就格外刺眼。HyperFrame 本质上是一次及时的技术还债。如果你手头有长期维护的数据项目我给的建议是不要等到新架构落地再行动。现在就打开你的 pandas开启 CoW把测试用例跑一遍看看有哪些代码“悄悄地依赖了旧行为”。我一开始切换 CoW 时就发现自己好几段数据处理代码其实一直在吃共享引用的红利修完之后代码不但能跑内存还降了将近三分一。最后再分享一个小技巧把 pandas 升级到 2.2 之后遇到可疑的内存问题时用pd.show_versions()确认版本再用df.info(memory_usagedeep)做初步定位最后用小样本数据跑一遍tracemalloc。这套组合能解决绝大多数“内存莫名爆掉”的困惑。技术重构听起来很远但踩坑的经验永远都是你现在就能用的。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询