NumPy17/18
Starting sandbox…
Beginner·4 graded tests

NumPy ndarray

ndarray

Core Principle: The ndarray object 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

[1]python
out1ms
  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:

  1. Homogeneous. Every element is the same dtype. → No mixed-type columns; that is what Pandas is for.
  2. Fixed size. The buffer is allocated once. → np.append reallocates and copies the whole array; never append in a loop. Build a list, then convert once, or preallocate with np.empty.
  3. N-dimensional and rectangular. No ragged rows. → Sequences of different lengths must be padded, or held in a list.
  4. 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:

[2]python
out1ms
(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:

[3]python
out1ms
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.append or np.concatenate: quadratic time. Collect in a list, convert once.
  • Ignoring integer overflow. np.array([127], dtype=np.int8) + 1 gives -128, no warning. Read counts belong in int32 or int64.
  • Using np.empty and 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 ==. Use np.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.

exerciseEvery submission runs independently in a fresh namespace.

Test cases · 4

#viainputexpected stdout
1stdin1 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
2stdin1.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
3stdin7 shape=1x1 ndim=2 size=1 dtype=int64 itemsize=8 nbytes=8 flat_shares=True tflat_shares=True total=7.0
4stdin1 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.