3.1 Reshaping, Stacking, and Splitting Arrays
Chapter 2 treated shape as part of an array's meaning. This section turns that idea into a practical rule: before changing a shape, write down what every axis means after the change. NumPy can verify element counts, but it cannot tell whether a row means a date, a shift, or a delivery zone.
We will use a flat shift log containing 24 measurements:
import numpy as np
flat = np.arange(24)Imagine the measurements arrived in this order: two days, within each day three shifts, and within each shift four zones. The one-dimensional array has all the values but hides those coordinates.
Reshape changes coordinates, not the observations
reshape returns an array with a requested shape while preserving the number of elements:
shift_log = flat.reshape(2, 3, 4)
print(shift_log.shape) # (2, 3, 4)
print(shift_log[1, 2, 1])Here axis 0 is day, axis 1 is shift, and axis 2 is zone. The coordinate [1, 2, 1] therefore means the second day, third shift, and second zone. In the default row-major, or C order, the final axis changes fastest. Its flat position is
The value is consequently flat[21]. This arithmetic is useful for debugging data that looks plausible but was reshaped using the wrong axis contract.
The old and new shapes must have equal products:
flat.reshape(4, 6) # valid: 4 * 6 = 24
flat.reshape(3, 8) # valid: 3 * 8 = 24
flat.reshape(5, 5) # ValueError: 5 * 5 != 24One dimension may be -1; NumPy infers it from the known dimensions:
by_zone = flat.reshape(3, -1)
print(by_zone.shape) # (3, 8)Only one dimension can be inferred because two unknown dimensions do not determine a unique shape. Also remember that a mathematically valid reshape can still be semantically wrong. Both (2, 3, 4) and (4, 3, 2) hold 24 values, but they describe different coordinate systems.
Flattening and transposing answer different questions
ravel and flatten both produce one-dimensional results, but they express different ownership intentions:
possibly_shared = shift_log.ravel()
independent = shift_log.flatten()ravel returns a view when it can, so its result may share storage with the source. flatten always returns a copy. Section 3.4 will make that difference visible and show how to test memory sharing.
transpose does not merely flatten and refill the array. It reorders axes:
zone_first = shift_log.transpose(2, 0, 1)
print(zone_first.shape) # (4, 2, 3)The new axis contract is zone × day × shift. The same observation previously addressed as shift_log[1, 2, 1] is now zone_first[1, 1, 2]. Axis labels move with the axes; the observation does not acquire a new meaning.
Concatenate extends an axis; stack creates one
Suppose two weekly files have already been validated and converted to arrays of shape (2, 3). Rows are days and columns are three zones:
week_a = np.array([
[31, 38, 44],
[29, 41, 47],
])
week_b = np.array([
[33, 39, 42],
[35, 40, 46],
])Concatenation joins arrays along an axis that already exists:
longer_week = np.concatenate([week_a, week_b], axis=0)
more_columns = np.concatenate([week_a, week_b], axis=1)
print(longer_week.shape) # (4, 3)
print(more_columns.shape) # (2, 6)Along axis=0, every non-concatenated dimension must match, so both arrays need three columns. Along axis=1, both need two rows. A mismatch raises an error rather than silently inventing alignment.
Stacking inserts a new axis:
weekly_batches = np.stack([week_a, week_b], axis=0)
print(weekly_batches.shape) # (2, 2, 3)The axes now mean batch × day × zone. np.vstack and np.hstack are conveniences whose behavior depends on input dimensionality; in analytical code, explicit concatenate(..., axis=...) or stack(..., axis=...) often communicates intent more clearly.
Before combining arrays, check three things:
- Do corresponding axes describe the same entities and units?
- Do all dimensions other than the concatenation axis match?
- Is a new source or batch axis needed, or should an existing row/column axis grow?
Split with boundaries you can explain
np.split reverses a concatenation when the division is exact:
first_two_days, last_two_days = np.split(longer_week, [2], axis=0)
rebuilt = np.concatenate([first_two_days, last_two_days], axis=0)
assert np.array_equal(rebuilt, longer_week)The list [2] contains a boundary position, not a piece size. Multiple boundaries make multiple pieces:
first, middle, final = np.split(np.arange(10), [3, 7])
# first: positions 0:3, middle: 3:7, final: 7:10Passing an integer instead requests an equal number of pieces. np.split(values, 3) fails when the selected axis length is not divisible by three. np.array_split(values, 3) permits uneven pieces, placing extra elements in earlier pieces. That convenience is appropriate only when uneven groups are acceptable in the domain.
Never infer a business partition from convenient array arithmetic alone. If a train/test boundary, customer group, or time cutoff matters, store that boundary and explain how it was chosen.
You can now change an array's layout without losing its axis contract. The next section uses reusable permutations and ordered boundaries to rearrange records while preserving the relationships between columns.