Stacking and Splitting

Joining arrays along an existing axis or a new one - and why the two are different functions.

Overview

Two different questions

Combining arrays is something you do constantly. The data arrives in pieces, and at some point you need a single array that holds all of it. NumPy gives you two families of functions for this, and choosing the wrong one gives you an array with the wrong number of dimensions.

Concatenate joins arrays along an existing axis. Two (3, 4) arrays concatenated on axis 0 give (6, 4). The axis already existed. It just got longer. That is the operation you want when stacking more rows onto a matrix or appending more time steps to a signal.

Stack joins arrays along a new axis. Two (3, 4) arrays stacked give (2, 3, 4). A new axis of length 2 was added at the front. That is the operation you want when collecting separate images into a batch or collecting separate feature vectors into a matrix.

The difference is exactly one dimension. After concatenate, ndim stays the same. After stack, ndim goes up by one. If your result has an unexpected extra dimension, you probably used stack when you meant concatenate, or vice versa.

hstack and vstack are convenience wrappers. vstack is concatenate along axis 0. hstack is concatenate along axis 1. They exist because the axis parameter can be confusing, and "vertical" and "horizontal" are easier to picture. column_stack handles the common case of turning 1-D arrays into columns of a 2-D array.

Splitting is the reverse of stacking. np.split divides an array into equal pieces along an axis. np.array_split handles unequal pieces. hsplit and vsplit are the convenience wrappers. These are less commonly used, but they appear when you need to process chunks of data separately.

This module covers concatenate, stack, hstack, vstack, column_stack, split, and the common patterns for combining and dividing arrays. The examples show the shape of the result in each case, because the shape is the thing you need to verify.

You've a few arrays and want one. There are two distinct things that could mean. NumPy gives them split functions rather than guessing.

**concatenate** joins along an axis that already exists. Two (2, 3) arrays concatenated on axis 0 give (4, 3). More rows, same number of dims.

**stack** creates a new axis. The same two arrays stacked give (2, 2, 3). A pile of two frames, one dim more than the inputs.

Asking which you want is the first step. The answer is often obvious once phrased that way. Appending records is joining. Collecting frames or samples into a batch is stacking.

Worth knowing

concatenate joins along an existing axis; stack creates a new one.
For concatenate, every axis except the joining one must match. For stack, the shapes must be identical.
vstack and hstack are conveniences. On 1-D input they differ sharply: vstack makes rows, hstack joins end to end.
column_stack turns 1-D arrays into columns, which is usually what people reach for hstack hoping to get.
split requires equal parts and raises otherwise; array_split allows uneven ones.
Never concatenate in a loop — each call copies everything so far. Collect into a list and stack once.

Stacking and Splitting

Joining along an existing axis or a new one. Why those are other functions.

concatenate joins along an existing axis

The arrays must already agree on every other axis. Nothing new is created.

example_01.pyNumPy
Output

stack creates a NEW axis

That is the whole difference. Same inputs, one more dimension out.

example_02.pyNumPy
Output

vstack, hstack and the 1-D surprise

Convenient names, and a special case for 1-D input that catches people.

example_03.pyNumPy
Output

Shapes must line up

The error names the axis that disagrees, which is usually enough to find the problem.

example_04.pyNumPy
Output

Splitting is the inverse

split needs equal parts; array_split does not.

example_05.pyNumPy
Output

Building in a loop is still wrong

Concatenating repeatedly is quadratic for the same reason appending is. Collect, then join once.

example_06.pyNumPy
Output

What must match

concatenate needs each axis except the joining one to agree. Joining on axis 0 needs matching column counts. Joining on axis 1 needs matching row counts.

stack is stricter: all inputs must have just the same shape. They're turning parallel slices of a new axis.

The error messages name the mismatch. Reading them rather than guessing often locates the problem at once.

The convenience functions

vstack stacks vertically, hstack flatly, dstack along the third axis. For 2-D input they're just concatenate with a fixed axis.

For 1-D input they diverge, and this is where they cause trouble.

np.vstack([a, b]) on two length-3 arrays gives (2, 3). It promotes each to a row.

np.hstack([a, b]) gives (6,) — it joins them end to end. Axis 1 doesn't exist so it falls back to axis 0.

People reaching for hstack to make two columns get one long array instead. column_stack is the function that does what they meant. It turns 1-D arrays into columns of a 2-D result.

The general advice: use concatenate or stack with an clear axis when the dimensionality might vary. The convenience names are fine when you know just what shapes you've.

Splitting

np.split(a, n, axis=0) cuts into n equal parts and raises if the length doesn't divide just. That strictness is handy. An uneven split is often a sign that a thing upstream isn't the size you assumed.

np.array_split allows uneven parts, distributing the remainder across the first few. Use it when uneven is truly acceptable, such as chunking work for parallel handling.

split also accepts a list of spots rather than a count: np.split(a, [3, 7]) cuts before index 3 and before index 7, giving three pieces. That is often more natural than finding a count.

The pieces are views, not copies. This Is worth knowing. Splitting a large array is free. Writing into a piece writes into the first.

Do not build in a loop

The same rule as np.append, for the same reason.

Concatenating inside a loop allocates a new array and copies all built up so far, on each loop. That is quadratic. The last editor measures the gap.

Collect into a Python list and call stack or concatenate once at the end. Lists append cheaply. Arrays don't append at all.

If you know the final size in advance, better still. Allocate with np.empty and assign into slices. That avoids even the single large copy.

Choosing

Appending records of the same width: concatenate(axis=0).

Adding columns to a table: concatenate(axis=1), or column_stack for 1-D inputs.

Collecting frames, samples or channels into a batch: stack, with an clear axis.

Undoing any of those: split when the division is exact, array_split when it isn't.

And in each case, if the joining is happening inside a loop, the answer is to move it outside.

r_ and c_

np.r_ and np.c_ are index-formula shortcuts. They look strange because they use square brackets rather than parentheses.

np.r_[a, b] concatenates along the first axis. np.c_[a, b] stacks as columns, equal to column_stack.

They also accept slice syntax. So np.r_[0:5, 10, 20:23] builds an array from a mix of ranges and literals in one formula. That is truly convenient for making index arrays and test data.

They're compact rather than clear, and they show up more in older code and in interactive sessions than in libraries. Worth recognising. Not worth preferring over the named functions in code others will read.

tile and repeat

Both make an array bigger by duplication. They differ in a way that is easy to state and easy to forget.

np.tile(a, n) repeats the whole array n times: [1, 2] turns [1, 2, 1, 2].

np.repeat(a, n) repeats each element n times: [1, 2] turns [1, 1, 2, 2].

tile accepts a tuple to repeat along a few axes. This Is how you build a checkerboard or replicate a small pattern across a grid.

repeat accepts an axis and a per-element count. np.repeat(rows, counts, axis=0) expands a table per to a count column. The standard way to turn aggregated data back into one row per finding.

Neither is a substitute for broadcasting. If you're tiling an array purely so that its shape matches another one for an math step, broadcasting will do it without allocating anything. The tile is wasted memory.

Padding

np.pad(a, width, mode) adds elements around the edges.

width can be a single number, a pair for before and after, or a pair per axis. The nesting gets confusing quickly. It's worth checking the result's shape the first time on any new call.

The mode arg covers the cases that would else be fiddly: "constant" with a constant_values arg, "edge" to repeat the border, "reflect" and "even" to mirror, and "wrap" to tile.

This is how you run boundary rules for a conv or a stencil without writing an index-clamping branch. It's one of the functions that quietly drops a lot of code.

block, for nested assembly

np.block builds an array from a nested list of arrays, laid out the way the nesting reads.

np.block([[A, B],
          [C, D]])

makes the block mat, given the pieces have matching shapes. It's far clearer than the equal chain of hstack inside vstack. It's the right tool for assembling a mat from named submatrices. A co-var built from blocks, a system of equations assembled from parts.

Choosing among the joins

The functions share enough to be worth a summary.

**concatenate** — the general form. clear axis, works on any dimensionality, no surprises. Reach for this when the dimensionality might vary or when clarity matters.

**stack** — when a new axis is what you want. Collecting frames, samples or repeated runs into a batch.

**vstack / hstack / dstack**. Conveniences with a fixed axis. Safe on 2-D. Divergent on 1-D in a way that catches people. Fine when you know the shapes just.

**column_stack** — turns 1-D arrays into columns. This Is what people often want from hstack and don't get.

**block** — nested assembly from named pieces.

**r_ / c_** — terse, for interactive use.

And the rule that applies to all of them: none belongs inside a loop. Collect into a list, join once.

Splitting, in practice

The pieces returned by split are views. Splitting a large array is free and writing into a piece writes into the first. That is often convenient and at times surprising. It follows the same rules as any other slice.

array_split distributes an uneven remainder across the first pieces. Splitting 10 into 3 gives lengths 4, 3, 3. That is fixed. This Matters if two processes split the same data on its own and must agree.

For chunking work rather than cutting it evenly, finding the boundaries yourself and passing them as a list is often clearer than asking for a count. np.split(a, range(0, len(a), chunk)) cuts at fixed intervals even so of whether the total cuts evenly.

Joining arrays of different dtypes

Concatenating an int array with a float array gives a float result, following the same promotion rules as math.

That is often what you want. At times not. Joining an int64 array of identifiers with a float64 array of readings makes a float array in which large identifiers may no longer be just representable.

dtype= on concatenate forces the result type by name. casting="no" makes a mismatch an error rather than a silent promotion. In code where the dtype matters, being clear costs one arg and drops a whole group of surprise.

Joining a fixed-width string array with a longer one widens to fit. This Is the one place NumPy strings do the accommodating thing rather than truncating.

Building an output array up front

When the final size is known, neither joining nor appending is needed.

out = np.empty((n_rows, n_cols))
for i, row in enumerate(source):
    out[i] = compute(row)

One allocation, no copying, and the slices being assigned into are views. This is the fastest form when a loop is unavoidable, and it's clearer than building up pieces to join later.

np.empty is right here because each element is written. If some rows might be skipped, np.zeros or np.full(shape, np.nan) makes the unfilled entries visible rather than leaving what was in memory.

Preallocating also forces you to state the output shape. This Often surfaces an error in the thinking before any code runs.

Splitting for parallel work

np.array_split(a, n) is the natural way to divide data among n workers. It handles the case where the length doesn't divide evenly, distributing the remainder across the first pieces deterministically.

Because the pieces are views, splitting costs nothing. But views can't cross a process boundary. Multiprocessing will copy them anyway when pickling. For thread-based parallelism the views are shared right. This Is one of the reasons threads are attractive for NumPy work.

For chunking rather than cutting. Fixed-size pieces. Many that turns out to be — passing clear boundaries is clearer:

np.split(a, range(chunk, len(a), chunk))

That cuts each chunk elements and leaves a shorter final piece. This Is often what "process in batches of 1000" means.

The summary

Joining along an existing axis: concatenate, with an explicit axis.

Adding a new axis: stack, with an explicit axis.

1-D arrays into columns: column_stack, not hstack.

Assembling from named blocks: block.

Duplicating: tile for the whole array, repeat for each element — and neither if broadcasting would do the job without allocating.

Adding borders: pad, with the mode chosen for the boundary condition.

Dividing: split for exact division, array_split for uneven, explicit boundaries for fixed-size chunks.

And the rule that replaces all of them. None of these belongs inside a loop that runs once per element. Collect into a Python list and join once, or preallocate and assign into slices.

Questions people ask about Stacking and Splitting

What is the difference between concatenate and stack? concatenate joins along an existing axis. stack creates a new axis, so all inputs must have identical shapes.

When do I use vstack and hstack? vstack joins rows, hstack joins columns. They are shortcuts for concatenate on axis 0 and axis 1, with 1D inputs treated as rows.

Why does concatenate raise a dimension error? All axes except the one you join along must match. Print the shapes and find the one that differs.

How do I split an array into equal parts? np.split(a, n) needs the length to divide evenly. Use np.array_split when it may not, which gives parts of nearly equal size.

Do the pieces from split share memory? Yes, they are views into the original. Copy them if you plan to modify one.

How do I stack many arrays efficiently? Collect them in a list and make one concatenate call at the end. Repeatedly stacking two at a time copies the growing result each time.

A closing note on Stacking and Splitting

Joining arrays is one of the places where NumPy offers a few functions for what feels like one job. The abundance is more confusing than helpful at first.

The way through it's the question at the top of this module. Does the result have the same number of dims as the inputs, or one more? all else is a convenience wrapper over that choice. concatenate and stack with an clear axis will do any of it right if you'd rather not note the rest.

Check yourself

0 of 4

Answer without scrolling back up.

  1. What is the difference between `concatenate` and `stack`?

  2. What does `np.hstack` do with two 1-D arrays of length 3?

  3. How does `split` differ from `array_split`?

  4. Why not concatenate inside a loop?

Cheat sheet

Stacking and Splitting

Join NumPy arrays with vstack, hstack and concatenate. Split them with array_split and see when to stack along a new axis instead.

NUMPY · vizlearn.in/numpy/stacking_and_splitting.html

About the author

Ashish Jangra builds and maintains VizLearn. Every module here is written and the visualisation behind it hand-built, so the numbers in a readout come from the same code that draws the picture. Corrections are genuinely welcome and get priority over everything else — if a page states something wrong, or an animation misrepresents what the algorithm does, get in touch.