尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
Numpy矩阵合并详解:vstack、hstack与concatenate的用法与性能优化
做了几年数值计算相关的工作Numpy算是我用得最熟的工具之一。但真正让我愿意停下来认真写一篇关于矩阵合并的文章是因为我发现很多人在这个看起来无比基础的操作上反复踩同样的坑——维度理解错、合并方向反了、数据量一大就奇慢无比。尤其是刚接触Numpy的朋友很容易把合并操作当成“魔法”照着别人的代码抄一旦输出不对就完全没思路。Numpy里的矩阵合并本质上就做一件事把两个或更多数组按某个方向拼在一起。听着简单但它牵涉到的维度匹配、内存分配、轴axis理解才是真正拉开水平差距的地方。这篇文章不打算只列函数签名我会结合自己在实际项目中踩过的坑把vstack、hstack、concatenate、stack、append这些函数的用法、区别、性能问题一次性讲透帮你以后遇到合并需求时能直接做出正确的选型和排错。1. 矩阵合并的底层逻辑一切都在围绕“轴”转1.1 合并的本质是沿着某条轴拼接我们经常把Numpy数组想象成一个表格说“上下合并”、“左右合并”这当然没错。但落到代码层面Numpy并不识别“上下”和“左右”这种人类语言它唯一认的是轴。轴就是数组的每个维度二维数组有两个轴axis0是行方向axis1是列方向三维数组则有三个轴展开来说就是“块”、“行”、“列”三个层次。理解了这一点再回头看合并就特别清晰你要合并数组必须先告诉Numpy“沿着哪条轴把数据拼起来”。用上下合并举例子意思就是沿着行方向也就是axis0把两个数组“摞”起来。左右合并则是沿着列方向即axis1把两个数组“并排”起来。可能有人会问那为什么Numpy里还要专门提供vstack和hstack这两个函数直接用concatenate指定axis不就行了这里就牵扯到一个设计理念vstack和hstack本质上是用更直观的方式封装了concatenate的常用场景让代码更可读。vstack这个名字就是vertical stack的缩写表意明确hstack是horizontal stack同样一看就知道在干嘛。这种封装对提升代码的可读性帮助很大尤其是在团队协作时不用翻上下文就能猜出意图。1.2 一维数组没有“行”和“列”的概念一维数组是最容易在合并时产生困惑的对象。它只有一条轴axis0所以一维数组之间的合并本质上就是沿着唯一那条轴做拼接。举个例子a np.array([1, 2, 3])b np.array([4, 5, 6])。如果用np.vstack((a, b))得到的结果是[[1, 2, 3], [4, 5, 6]]形状变成了(2, 3)。原因很简单vstack的实现内部会先把一维数组统一转换成二维的(1, n)数组然后再做行方向的拼接。如果用np.concatnp.concatenate((a, b), axis0)会得到[1, 2, 3, 4, 5, 6]形状还是(6,)的一维数组。这里就引出一个值得特别注意的点当你需要把一堆一维数组拼成二维矩阵时vstack是更方便的。但如果你想保留一维数组的“原始性”做顺序拼接concatenate更直接。两种场景在实际代码里出现频率都很高需要自己判断。1.3 二维以上数组合并时的形状要求对于二维数组合并的规则可以用一句非常粗浅的话概括除了要合并的那条轴其他所有轴的维度必须完全一致。也就是说沿着axis0合并时要求两个数组的列数相同。因为你要上下拼上面的宽度和下面的宽度必须一样否则拼出来的矩阵不会是规则的矩形。同理沿着axis1合并时要求两个数组的行数相同。这个规则推广到三维、四维也一样指定合并轴之外的维度都必须相等。这句话才是合并操作的核心判断依据比死记“行为要一致”更通用、更准确。2. vstack、hstack、concatenate、stack、append五个函数的选型差异2.1 vstack和hstack的使用场景vstack最经典的使用场景就是把多个行向量拼成一个矩阵。比如你在做机器学习特征工程时从每条样本里提取了一个特征向量训练集有10000条样本那自然就是循环收集10000个行向量最后用vstack拼成一个(10000, n)的矩阵。这个操作非常高频属于那种写着写着就自然出现的代码。hstack的使用场景则正好反过来。当你手里有几组特征矩阵列数不同但样本数量相同想把这几个矩阵横着并起来作为最终的特征矩阵那就是hstack的活。比如说一个特征矩阵是形状(10000, 3)的速度特征另一个是形状(10000, 5)的加速度特征hstack之后就得到(10000, 8)的完整特征。这叫特征拼接在数据处理流水线里十分常见。对比之下有个容易混淆的点vstack的输入往往倾向于用元组包起来很多教程也这么写。但传入列表效果完全一样。np.vstack([a, b])和np.vstack((a, b))没有任何差别。真正关键的是传给它的是一堆待合并数组而不是“数组 特别参数”那种感觉。函数内部会遍历这个元组或列表依次把每一个数组沿对应方向堆叠。2.2 concatenate才是最通用的合并接口concatenate是五个函数里最底层、最灵活的一个。它接受一个待合并数组的序列再接受一个axis参数默认是axis0完全由你来控制合并方向。举几个例子import numpy as np a np.array([[1, 2], [3, 4]]) b np.array([[5, 6], [7, 8]]) # 上下合并结果形状(4, 2) c np.concatenate((a, b), axis0) # 左右合并结果形状(2, 4) d np.concatenate((a, b), axis1)上面的代码输出结果应该很好预测。c变成4行2列d变成2行4列。axis0这个参数我建议每一位初学者都打印出来看一看而不是在心里默背。因为在三维数组里axis的理解才是真正的分水岭。假设有一个数组形状是(2, 3, 4)你可以把它想象成两块形状为(3, 4)的数据叠在一起。axis0合并意思是往“块”方向摞也就是增加块数axis1合并是每块内的行方向拼axis2合并是每块内的列方向拼。维度升上去之后单纯用“上下左右”去理解就会失效但用“沿着第几条轴”去理解就永远不会错。2.3 stack不改变维度本质的“分组拼接”stack和前面的函数有个根本区别它会在合并时增加一个新的维度。这是它和concatenate最大的分水岭。np.stack((a, b), axis0)的意思是把a和b作为两个独立个体放进一个新的数组里新数组的第一个维度长度等于待合并数组的数量。举例来说两个(3, 4)的二维数组用stack合并axis0会得到(2, 3, 4)的三维数组axis1会得到(3, 2, 4)的三维数组axis2会得到(3, 4, 2)的三维数组。这种操作看名字就能猜出它的用途把一组同形状的数组“堆成一批”。比如你在做图像批处理时每个样本是(32, 32)的灰度图你想把1000个样本组织成一个(1000, 32, 32)的batchstack就是最合适的工具。直接把每个图像作为独立个体摞起来保留它们的分组边界。如果你问为什么不能用concatenate配合reshape答案是可以但步骤繁琐得多。stack的意义就是把“扩展维度 拼接”这个组合操作封装成一步既少了出错的可能又让意图更清晰。2.4 append看似方便实则最容易出错append这个函数在Python的原生列表里特别常用很多人带着这个习惯转到Numpy第一反应就是找np.append。但我要提醒你np.append的语义和原生列表并不完全一样。np.append(arr, values, axisNone)在没有指定axis的时候会先把arr和values拉平成一维数组再顺序拼接。这意味着np.append(np.array([[1, 2], [3, 4]]), np.array([[5, 6]]))不会得到你想象中的二维扩展而是会得到[1, 2, 3, 4, 5, 6]这种一维数组。很多人踩的第一个坑就在这里——append以后维数莫名其妙降了。指定axis之后append的行为其实跟concatenate非常接近只是它把“待合并数组的序列”变成了“arr和values两个参数”的语法。所以我的建议很直接如果真的要用append务必显式传入axis如果原本就是两个数组要拼接直接上concatenate别让append把代码搞得不清不楚。3. 合并操作的隐藏规则广播与维度变换对结果的影响3.1 广播机制为什么会让合并变得“反直觉”Numpy的广播机制本来是用于算术运算的让不同形状的数组能进行计算但它在合并时也偶尔跑出来捣鬼。常见的一种错觉是既然广播能让(3, 1)和(1, 4)的数组相加得到(3, 4)那感觉上它们也能被合并成(3, 4)。这个想法是错的因为合并和算术运算走的是两套规则。广播处理的是“维度不对齐但要参与运算”的情况合并处理的是“维度必须精确匹配只沿着指定轴增加长度”的情况。二者本质不同。所以在做矩阵合并前不要下意识地认为“广播能解决的事情合并也能解决”。合并更严格它要求待拼接的各数组除了拼接轴以外的所有维度严格相等这个条件没有任何“自动补全”机制。我遇到过不少同学在跑深度学习数据预处理时想要把不同长度的时间序列合并成一个大矩阵觉得Numpy应该“像广播一样自动把短的补成和长的一样”。这就是典型的混淆。Numpy的合并不会自动填充它只会直接报错。如果需要填充你要自己先手动处理对齐问题比如用pad补齐长度然后再合并。3.2 reshape在合并前的黄金用法由于合并要求形状严格匹配你在正式合并之前大概率需要做一次维度整理。这里最常用、最重要的操作就是reshape和newaxis扩展维度。举个例子。假设你有多个一维数组想用concatenate沿着axis1合并成二维矩阵。直接执行np.concatenate((a, b), axis1)在旧版Numpy中会报错因为一维数组没有axis1。解决办法是先把每个一维数组通过a[:, np.newaxis]变成( n, 1)的二维数组再去做concatenate。更优雅的一种方案是用np.expand_dims或者直接用np.column_stack这种专门处理列方向合并的函数。column_stack会内部自动把一维数组转成列向量再合并省去了手动reshape的麻烦。同理row_stack是vstack的别名用来处理“拼行”的情况很顺手。我自己的经验是当你发现合并代码里要写一堆reshape时先停下来想想是不是选错了合并函数换一个更贴合需求的函数往往能省掉一半的维度处理代码。3.3 dtype不匹配时的隐式转换合并时另一个容易被忽略的问题是数据类型。Numpy数组是有严格dtype的两个数组的dtype不同时合并并不会报错但会在背后做隐式类型提升。以一个很实际的现象为例一个int32数组和一个float64数组合并最终结果会变成float64。因为float64精度更高Numpy在合并时会自动选择能同时容纳两种数据类型的“更大”类型。这种隐式转换平时你不会察觉但如果处理的是大规模数据内存占用会突然翻倍涨性能敏感场景下尤其需要注意。还有一种隐患是你原本以为自己做的是“整数运算”结果因为某个数组混入了浮点数后面所有输出都变成了浮点数最终结果跟预期不符还很难定位。排查方法很简单合并后看一眼result.dtype确认是否符合预期这在强数据类型要求的项目里是一个值得养成的习惯。4. 性能与内存为什么不要用循环拼接4.1 每次合并都会产生全新数组Numpy数组和Python列表最大的不同之一是Numpy的数组合并总会生成一份全新的数据而不是在原数组上“追加”。这意味着每一次np.concatenate都会申请新的内存把旧数据复制过去再释放旧的内存。如果你在循环里一次次地做合并比如总共要合并10000个小数组那就会申请10000次新内存复制的数据量越来越大。第一轮复制一个小数组第二轮复制两个小数组的大小第一百轮复制一百个。循环越多累计复制的数据量就越大。这个复杂度其实是O(n^2)级别的数据量小的时候没感觉数据量一大肉眼可见地卡顿。这个性能陷阱我在实际项目里踩得刻骨铭心。当时我在处理一批传感器数据每个时间窗口生成一个小数组我需要把它们全部拼成一个完整矩阵。最初的代码就是循环里np.append数据量大概是5万多个窗口结果程序跑了好几分钟还没结束。后来改成先收集到Python列表最后一次性concatenate耗时直接降到零点几秒。两者差距悬殊到让人怀疑是不是写错了。4.2 正确的姿势先收集列表再一次性合并这里说的“正确姿势”其实是一个很经典的优化策略先用Python原生列表暂存所有待合并数组利用列表的append操作最后只用一次np.concatenate或np.vstack完成合并。为什么这样效率高因为Python列表的append是原地操作只负责追加指针引用并不会复制整个数据。而最后的一次性concatenate哪怕要复制全部数据总共也只复制一遍。对比循环里疯狂重复复制这个优化收益非常可观。下面是一段对比示例帮助理解import numpy as np import time # 低效写法循环内用 np.append arr np.zeros((0, 128)) start time.time() for i in range(10000): row np.random.randn(1, 128) arr np.append(arr, row, axis0) print(循环 append 耗时:, time.time() - start) # 高效写法列表收集 一次性合并 chunks [] start time.time() for i in range(10000): row np.random.randn(1, 128) chunks.append(row) result np.vstack(chunks) print(列表收集 vstack 耗时:, time.time() - start)实跑下来低效写法在数据量大的时候能慢几十倍甚至上百倍。这是一个值得刻进肌肉记忆的性能准则合并的调用次数越少越好。4.3 终极优化预分配数组加切片写入如果你的场景里数组数量是提前知道的那么最极致的做法是预分配一个目标大小的空数组然后循环往里填数据。这个做法不依赖任何合并函数只靠Numpy的切片赋值。n_rows 10000 result np.zeros((n_rows, 128)) for i in range(n_rows): result[i] np.random.randn(128)这样做的好处是只分配一次内存不产生任何临时大数组同样不存在重复复制。它的执行效率比“列表收集 vstack”通常还要略高一点因为vstack内部也要经历一次完整的数据复制而预分配方案省掉了这次复制的来源组织过程。不过预分配方案也有自己的局限它要求你提前知道最终的行数。实际操作中如果行数不确定可以用先收集列表再合并的方案灵活性更高。两类方式组合起来基本能覆盖绝大多数合并需求。5. 我踩过的合并坑异常信息与排查全过程5.1 最常见的ValueError维度不一致第一次运行合并代码时我相信每个人都见过类似这样的报错ValueError: all the input array dimensions except for the concatenation axis must match exactly这个报错说的是除了拼接轴之外其余维度必须严格相等。它是合并操作里最常见的运行时错误。但报错信息其实不会告诉你具体是哪个维度对不上所以你得自己排查。我一般会用一个非常笨但有效的方法把每个待合并数组的shape逐个打印出来。尤其是在批量处理数据时可能某个数组形状异常其他几百个数组都是正常的。如果只盯着报错那次看花半天也未必找得到问题源头。写代码时加上一段临时的shape检查for i, arr in enumerate(arrays): print(i, arr.shape)跑一轮就能定位到异常数组效率极高。这个习惯帮我省下了大量时间。5.2 axis参数理解错误导致结果形状不对还有一个更隐蔽的坑就是axis选错导致的“结果能跑但不符合预期”。它不会报任何错误因为维度条件都满足只是合并方向错位了。举例说明有两个形状都是(3, 3)的矩阵你原本想左右合并得到(3, 6)但axis写成了0结果得到(6, 3)。如果后续代码没有对形状做强校验这个错误很可能一路传播到最终结果直到出图或输出文件时才暴露到时候定位成本就非常高了。我的建议是在手写合并操作时先心算一遍结果的shape再打印确认。result.shape是一个比任何断言都简单的检查手段。如果你经常搞混可以把常用场景写成一个小的辅助函数把函数的意图和合并方向绑定在一起减少手滑的可能。5.3 一维数组与二维数组合并的“伪失败”处理非结构化数据时经常会遇到一维数组和二维数组之间的合并需求。直接拿一维数组和二维数组做concatenate如果axis1Numpy会报维度错误因为一维数组没有第二个维度。这种情境下很多人的第一反应是reshape但我觉得更顺手的方案是借助vstack或row_stack。因为它们内部会自动把一维数组转成二维行向量省去手写reshape的步骤。不过要留意结果形状是否符合预期一个形状(3,)的一维数组如果用vstack和形状(2, 3)的二维数组合并最终结果是(3, 3)因为一维数组被当成了一行。如果你想让一维数组变成一列参与合并就得主动用arr.reshape(-1, 1)或者用arr[:, np.newaxis]扩展维度。这种场景下没有哪个函数能自动判断你是想“变行”还是“变列”需要靠人的理解去指定。5.4 多数组合并数量与内存扩展的边界一次性合并几千个数组在性能上比循环合并好很多但也需要注意输入序列本身的内存占用。比如你有一个列表里面存了几万个中等尺寸的Numpy数组这列表本身就已经占了不少内存。再把这些数组交给concatenateconcatenate还需要额外分配一块最终结果大小的内存于是同时存在“原始数组内存 待分配结果内存”两份数据。如果机器内存本来就不太宽裕这种操作有可能直接触发内存错误。碰到这种情况我会改用分块合并策略先把数组每1000个分成一组合并得到中间结果再把中间结果合并成最终结果。这样即使在内存受限的环境下也能稳定完成大规模合并任务只是代码会稍微多几行。6. 更进阶的合并思路从具体问题反推函数选择6.1 按需求维度选函数现在把前面讲的内容汇总成一张选择表实际开发时可以直接对照着做判断。需求特征推荐函数合并后的维度变化多个行向量拼成矩阵vstack / row_stack(n,) 系列变 (N, n)多个列向量拼成矩阵column_stack / hstack(n,) 系列变 (n, N)沿指定轴拼二维数组concatenate指定轴相加其他不变保持分组边界堆成新维度stack新增一个维度简单追加单个数组append必须指定axis与concatenate一致不需要马上合并待数据齐了再拼先列表收集再一次性concatenate与选择的基础函数一致这张表只列了常见场景。碰到比较罕见的维度组合回到第1章讲的“轴”概念去推演比硬套表格更可靠。6.2 合并顺序会影响性能吗当需要合并的数组数量非常多时除了“一次合并”还是“分多次合并”的选择合并顺序也会影响临时内存占用。如果你先合并大的数组再添加小的数组那么在合并过程中Numpy需要为“大数组小数组”的结果分配一块几乎和大数组一样大的新内存再加上原来的大数组内存就翻倍了。如果分批做虽然时间上不会快太多但瞬时内存峰值明显降低。我在内存有限的工作站上处理过一批总量接近几个GB的数据当时的做法是按时间先后切成几十个分片每一个分片内部先合并成中等数组最后再把几十个中等数组合并成最终结果。整个过程内存占用很平滑没有出现任何OOM内存不足报错。6.3 合并之后数据视图与副本合并操作返回的新数组通常是原始数据的副本而不是视图。这意味着你对合并结果做修改不会影响原来的数组。这与NumPy里切片操作返回视图的逻辑完全不同是一个容易让人迷惑的点。a np.array([[1, 2], [3, 4]]) b np.array([[5, 6], [7, 8]]) c np.concatenate((a, b), axis0) c[0, 0] 999 print(a[0, 0]) # 仍然是 1a 没变很多人会因此浪费不必要的内存因为副本无法共享底层数据。如果你只是想临时把两个数组组织在一起做批量处理可以考虑用列表包住它们而不是真的拼接出全新数组。这样能在不需要数据合并时省掉一次完整拷贝这个技巧在内存敏感场景里很实用。7. 写在最后合并只是起点学会控制内存才算入门回头再看矩阵合并这件事它真正难的地方从来不是函数参数怎么写而是你能不能清楚地知道每次操作背后发生了什么。合并会复制数据复制就带来时间和内存开销axis决定方向方向错了结果就彻底错dtype决定精度精度变了结果可能悄悄偏离预期。我自己在代码评审里最常说一句话先确认shape再讨论性能。很多合并相关的bug只要打印出中间结果的shape就能立刻暴露。所有藏着的问题都没有那么神秘要么是维度没对上要么是方向没搞对要么是触发了几何级数的内存复制。如果你正好在写数据处理流程我建议你做一个小实验把自己代码里所有np.append替换成“列表收集一次性concatenate”再看一遍执行时间和内存占用。我保证你会回来感谢这次重构。合并是Numpy操作里的基本功但基本功做扎实了后面才能扛得住更大规模的数据。
RELATED

相关推荐

敏感数据动态脱敏:湖仓一体架构下针对手机号与金额的行级列级隔离

敏感数据动态脱敏:湖仓一体架构下针对手机号与金额的行级列级隔离

在数据合规监管(如个人信息保护法 PIPL、GDPR)日益收紧的今天,数据团队常常处于两难的“走钢丝”境地:业务分析师需要访问明细数据排查订单转化率,算法工程师需要高频拉取用户特征训练推荐模型,而安全合规团…

📅 2026/10/12 17:45:07
OpenCV contrib 4.5.3 预编译包配置指南:VS2022与CMake避坑实践

OpenCV contrib 4.5.3 预编译包配置指南:VS2022与CMake避坑实践

简介:一套已编译好的OpenCV contrib 4.5.3 C资源包,面向需要快速配置OpenCV开发环境的C开发者。自行编译contrib模块常因CMake配置、依赖缺失等问题失败,这份资源直接提供完整编译产物,尤其适合在Windows平台进行图像处理、计算机…

📅 2026/10/12 17:45:07
CubeFS 仓库中的 Go 泛型结构解码库 mapstructure:从 map[string]interface{} 到原生结构体的完整实战指南

CubeFS 仓库中的 Go 泛型结构解码库 mapstructure:从 map[string]interface{} 到原生结构体的完整实战指南

存储分布式文件系统对象存储云原生 【免费下载链接】cubefs cloud-native distributed storage 项目地址: https://gitcode.com/gh_mirrors/cu/cubefs 点击查看 免费下载 导读 mapstructure 是 Go 社区广为使用的一个反射解码库,核心能力是把 map[stri…

📅 2026/10/12 17:40:06
MORE NEWS

更多资讯

📰

ThreadLocalRandom vs Random:高并发随机数生成性能优化实战

1. 为什么需要重新认识ThreadLocalRandom日常写Java代码,只要涉及随机数,绝大多数人第一反应就是Math.random()或者new Random()。这两个东西用起来确实顺手,但如果你稍微翻一下JDK源码,会发现Math.random()内部其实维护了一个静态…

📰

Isaac Gym机械臂强化学习:并行训练与演示引导实战

简介:面向深度强化学习研究与相关专业毕业设计、课程项目的综合资源包,主体为基于 NVIDIA Isaac Gym 的机械臂操控训练框架,集成了 SAC2019 与演示引导强化学习算法,适合需要快速搭建仿真训练环境、开展策略对比实验的开发者。压缩…

📰

新概念英语第四册第9课精读:从间谍叙事到写作迁移

《新概念英语》第四册第9课 Royal espionage,标题看起来像一部谍战小说,翻开内容讲的却是几百年前一位国王亲自当间谍的旧事。我第一次读这篇课文时,心里想的其实是“这哪里是间谍故事,分明是一篇历史评论文”;多读几遍…

📰

Flask多租户SaaS权限体系实战:从数据隔离到RBAC落地

1. 多租户SaaS权限体系到底难在哪做过单机Web应用的人第一次接触多租户SaaS,最容易犯的错就是把它当成"给每张表加个tenant_id就完事"。我最早接手一个内部工单系统改造成SaaS版本时也是这么想的,结果上线两周就被客户投诉数据串了——A公司的…

📰

Python肝脏CT图像分割与三维重建全流程实战详解

简介:一份基于Python的肝脏CT图像分割与三维重建项目资源,面向医学图像处理、计算机视觉及相关专业的学生和开发者,可应用于毕业设计、课程设计、期末大作业或竞赛立项演示。资源包含完整源码与预训练模型,覆盖从肝脏CT预处理、分…

📰

Mangos服务端改端利器:物品、任务、BOSS编辑器实战详解

简介:这是一款面向Mangos魔兽世界模拟器服务端开发与维护者的数据编辑工具包,用于解决物品、任务、BOSS、NPC等核心数据手工修改数据库时繁琐易错的问题。压缩包共66个文件,大小仅1.63MB,内含主程序及依赖库,并配有55个…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬