Skillversitas

Reshaping and Manipulating Arrays

Reshaping and arrangement functions change an array's shape - or build new arrays from existing ones - without touching the underlying values. The central distinction is view versus copy: reshape and transpose can return windows over the same memory (free), while joins like concatenate and tile build fresh allocations. Knowing which is which is the performance version of the view/copy rule from the indexing chapter.

Reshaping

import numpy as np

a = np.arange(12)
b = a.reshape(3, 4)          # view - same data, new shape
c = a.reshape(4, 3)          # also fine - size must match
d = a.reshape(3, -1)         # -1: infer that axis - 12/3 = 4
print(b.shape, d.shape)
print(np.shares_memory(a, b))   # True - b is a view of a

# when a view is impossible (non-contiguous input), reshape copies
m = np.arange(12).reshape(3, 4)
t = m.T
print(np.shares_memory(t, t.reshape(4, 3)))  # reshape of a transpose: still a view if possible

flat = b.ravel()             # 1-D view when possible
flat2 = b.flatten()          # always a 1-D copy

reshape is the workhorse: the total element count must be preserved, and -1 in one axis position lets NumPy infer that length. It returns a view when the layout allows (contiguous memory), otherwise a copy - the same "try a view, copy if you must" rule as slicing. ravel flattens with the view-when-possible behavior, flatten always copies, and np.atleast_1d/2d/3d guarantee a minimum number of axes (useful in generic code that must handle scalars).

Transposing and Permuting Axes

m = np.arange(24).reshape(2, 3, 4)
print(m.shape)              # (2, 3, 4)
print(m.T.shape)            # (4, 3, 2) - all axes reversed
print(m.transpose(1, 0, 2).shape)   # (3, 2, 4) - explicit permutation
print(m.swapaxes(0, 1).shape)       # (3, 2, 4) - swap two axes

T reverses all axes; transpose(*axes) takes an explicit permutation (each axis index exactly once); swapaxes(i, j) swaps two. All three return views - transposing a large array is instant because no data moves, only the shape and strides change. The standard use: aligning axes before an operation (a (batch, features) matrix to (features, batch) for a matrix product, an image from (H, W, C) to (C, H, W) for a model).

Joining: concatenate and stack

a = np.array([1, 2, 3])
b = np.array([4, 5, 6])

print(np.concatenate([a, b]))            # [1 2 3 4 5 6] - join along axis 0

m1 = np.ones((2, 3))
m2 = np.zeros((2, 3))
print(np.concatenate([m1, m2], axis=0).shape)   # (4, 3) - vertical
print(np.concatenate([m1, m2], axis=1).shape)   # (2, 6) - horizontal

print(np.stack([a, b]).shape)            # (2, 3) - new axis, side by side
print(np.column_stack([a, b]).shape)     # (3, 2) - as columns
print(np.vstack([a, b]).shape)           # (2, 3)
print(np.hstack([a, b]).shape)           # (6,) / (2, 3) for 2-D inputs

concatenate joins along an existing axis (all inputs must match on every other axis) and returns a new array. stack creates a new axis and places the inputs along it - the "make a batch from these rows" operation. vstack/hstack/column_stack are the 1-D/2-D conveniences. All of them allocate new memory - they are copies by construction.

Splitting

a = np.arange(12)
print(np.split(a, 3))                 # three equal chunks
print(np.split(a, [4, 8]))            # at explicit index boundaries

m = np.arange(24).reshape(4, 6)
print(np.hsplit(m, 2)[0].shape)       # (4, 3) - left half
print(np.vsplit(m, 2)[1].shape)       # (2, 6) - bottom half

The split family is the inverse of concatenate: split along axis 0, hsplit horizontally, vsplit vertically, with equal chunk counts or explicit boundary indices. They return views into the original where the layout allows - splitting is cheap in the same way reshaping is.

tile, repeat, and pad

a = np.array([1, 2, 3])
print(np.tile(a, 3))              # [1 2 3 1 2 3 1 2 3] - repeat the whole array
print(np.tile(np.array([[1, 2]]), (2, 2)))   # 2x2 blocks of the row

print(np.repeat(a, 2))            # [1 1 2 2 3 3] - repeat each element
print(np.repeat(a, [2, 1, 3]))    # [1 1 2 3 3 3] - per-element counts

p = np.pad(a, 2, mode="edge")     # [1 1 1 2 3 3 3 3] - edge-value padding
print(p)
print(np.pad(np.array([[1, 2], [3, 4]]), 1, mode="constant", constant_values=0).shape)  # (4, 4)

tile repeats the array as a whole (a tiling pattern), repeat repeats individual elements (a run-length expansion), and pad adds a border with constant, edge, or symmetric values - the standard preprocessing step before convolutions and image analysis. All three return new arrays.

Controlling Copies Explicitly

x = np.arange(6).reshape(2, 3)
y = x.copy()                      # always an independent copy
print(np.shares_memory(x, y))     # False

z = np.ascontiguousarray(x.T)     # force a C-contiguous layout
print(z.flags["C_CONTIGUOUS"])    # True
w = np.asfortranarray(x)          # force Fortran (column-major) layout

copy() makes independence explicit - the answer to "I want this data to stop sharing memory". ascontiguousarray/asfortranarray return a copy with the requested memory order when the input is not already in it, which is the fix for the odd performance case where a non-contiguous slice or transpose is feeding a routine that wants sequential access.

Summary

Reshape with reshape (view when possible, -1 to infer), transpose with T/transpose/swapaxes (always free views), build batches with stack and joins with concatenate/vstack (always new memory), undo joins with the split family, and shape with tile/repeat/pad. Use copy() and ascontiguousarray when memory independence or layout is the requirement - and keep the reflex: view operations are free, join operations allocate.