NumPy ndarray
ndarray
Core Principle: The
ndarrayobject combines a contiguous memory buffer with structural metadata (shape, dtype, and strides), establishing the foundation for all high-performance array operations in NumPy.
The anatomy
C_CONTIGUOUS : True F_CONTIGUOUS : False OWNDATA : False WRITEABLE : True ALIGNED : True WRITEBACKIFCOPY : False
a.base is the debugging tool people forget. If it is not None, you are holding a view and writes will propagate. See Views vs Copies.
The four constraints
These define what an ndarray is, and each one has a consequence you will feel:
- Homogeneous. Every element is the same dtype. → No mixed-type columns; that is what Pandas is for.
- Fixed size. The buffer is allocated once. →
np.appendreallocates and copies the whole array; never append in a loop. Build a list, then convert once, or preallocate withnp.empty. - N-dimensional and rectangular. No ragged rows. → Sequences of different lengths must be padded, or held in a list.
- Contiguous (usually). → Slicing is free; arbitrary reordering is not.
Shape is not a suggestion
Most NumPy errors are shape errors. Three shapes that look the same and are not:
(3, 1)
A (3,) array is neither a row nor a column. When you need one explicitly:
v[None, :] # (1, 3) also v[np.newaxis, :]
v[:, None] # (3, 1)
v.reshape(-1, 1)
The [:, None] idiom is the workhorse of Broadcasting: it is how you subtract a per-row value from every row of a matrix.
Creating one
Covered fully in Array Creation, but the essentials:
array([[0, 0, 0, 0],
[0, 0, 0, 0],
[0, 0, 0, 0]])
Bioinformatics framing
A (20000, 500) float32 expression matrix is 40 MB: trivially fast for whole-matrix operations, and catastrophically slow if you loop over its 10 million elements in Python.
counts = np.loadtxt("counts.tsv", skiprows=1, usecols=range(1, 501))
counts.shape # (20000, 500): genes × samples
cpm = counts / counts.sum(axis=0) * 1e6 # normalise per sample
logcpm = np.log2(cpm + 1)
gene_mean = logcpm.mean(axis=1) # per gene → shape (20000,)
Note axis=0 sums down columns (one total per sample) while axis=1 averages across rows (one value per gene). Getting this backwards silently produces plausible-looking nonsense. See Axes and Reductions.
Common mistakes
- Assuming
(n,)and(n,1)are interchangeable. They broadcast differently and produce silently wrong shapes. - Growing an array in a loop with
np.appendornp.concatenate: quadratic time. Collect in a list, convert once. - Ignoring integer overflow.
np.array([127], dtype=np.int8) + 1gives-128, no warning. Read counts belong inint32orint64. - Using
np.emptyand forgetting to fill it. The garbage looks like data. arr.reshape(...)without assigning. Reshape returns a new array; it does not mutate.arr.resize()does mutate, which is why it is rarely what you want.- Comparing floats with
==. Usenp.isclose.
See also
NumPy dtypes · Array Creation · Indexing and Slicing · Broadcasting · Views vs Copies · Memory Layout and Strides · NumPy
Exercise
Write describe(rows), which builds an array from a list of lists of string
tokens and reports on the object it produced. Return ["rows=0"] when there are
no rows and ["ragged"] when the rows are not all the same length, since an
ndarray is rectangular by definition.
Choose the dtype from the data: np.int64 when every token is an integer
literal, np.float64 otherwise. Then return four lines:
shape=<r>x<c> ndim=<n> size=<n>; dtype=<name> itemsize=<bytes> nbytes=<bytes> using dtype.name so the answer does not depend on how NumPy
spells the repr; flat_shares=<bool> tflat_shares=<bool>, whether reshaping the
array to one dimension and reshaping its transpose to one dimension each still
alias the original buffer; and total=<value>, the sum of every element rounded
to three decimals. The transpose of a matrix with more than one row and one
column cannot be flattened without a copy, but a degenerate one can.
Test cases · 4
| # | via | input | expected stdout |
|---|---|---|---|
| 1 | stdin | 1 2 3 4 5 6 | shape=2x3 ndim=2 size=6 dtype=int64 itemsize=8 nbytes=48 flat_shares=True tflat_shares=False total=21.0 |
| 2 | stdin | 1.5 2 3 4 5 6.25 | shape=2x3 ndim=2 size=6 dtype=float64 itemsize=8 nbytes=48 flat_shares=True tflat_shares=False total=21.75 |
| 3 | stdin | 7 | shape=1x1 ndim=2 size=1 dtype=int64 itemsize=8 nbytes=8 flat_shares=True tflat_shares=True total=7.0 |
| 4 | stdin | 1 2 3 4 5 | ragged |
Hints · 2
01Hint
A set of the row lengths tells you in one step whether the input is rectangular.
02Hint
np.shares_memory(a.T.reshape(-1), a) is False whenever the flattening had to copy, which is the usual case for a transpose.