NumPy01/18
Starting sandbox…
Beginner·4 graded tests

Array Creation

Array Creation

Core Principle: NumPy array creation allocates a fixed-size memory buffer according to specified dimensions and data types, prioritizing contiguous preallocation over dynamic array resizing.

From existing data

np.array([[1, 2], [3, 4]])            # copies by default
np.asarray(x)                          # no copy if x is already an ndarray
np.array(x, copy=False)                # NumPy 2: raises if a copy is needed
np.frombuffer(bytes_obj, dtype=np.uint8)

np.asarray is the right choice inside a function that accepts array-like inputs, supporting Python lists and NumPy arrays without redundant memory allocation when input is already an ndarray.

Filled with a constant

[1]python
out1ms
array([[1, 0, 0],
       [0, 2, 0],
       [0, 0, 3]])

np.empty is genuinely faster because it skips the fill, but only use it when you will overwrite every element. Otherwise np.full(shape, np.nan) is safer: unfilled cells show up as NaN instead of masquerading as plausible numbers.

The _like family

Matches shape and dtype of an existing array:

np.zeros_like(counts)
np.ones_like(counts)
np.full_like(counts, np.nan, dtype=np.float64)   # override dtype if needed
np.empty_like(counts)

Use these when building a result array in a pipeline: they keep dtype consistent without you restating it.

Ranges

[2]python
out1ms
array([   1.,   10.,  100., 1000.])

arange with a float step is a trap. Floating-point accumulation means the number of elements is not exactly predictable: np.arange(0, 0.3, 0.1) may give 3 or 4 elements depending on rounding. Use linspace whenever the step is not an integer.

Grids and coordinates

np.meshgrid(x, y)                  # coordinate matrices for surface plots
np.mgrid[0:5, 0:5]                 # same idea, slice syntax
np.indices((3, 4))                 # index arrays
np.ix_(rows, cols)                 # open mesh for cross-product indexing

np.ix_ is the clean way to pull a submatrix by row and column label lists:

sub = expr[np.ix_(gene_idx, sample_idx)]   # shape (len(gene_idx), len(sample_idx))

Without ix_, expr[gene_idx, sample_idx] would pair them elementwise instead: a classic Fancy Indexing mistake.

Random data

Always use the modern Generator API, never the legacy np.random.seed + np.random.rand globals. See NumPy Random Generator.

rng = np.random.default_rng(42)
rng.normal(0, 1, size=(3, 4))
rng.integers(0, 100, size=10)
rng.choice(genes, size=100, replace=False)

From files

np.loadtxt("matrix.tsv", delimiter="\t", skiprows=1)
np.genfromtxt("matrix.tsv", delimiter="\t", names=True, missing_values="NA")
np.load("array.npy")

For anything with mixed types, headers, or missing values, use pd.read_csv and then .to_numpy(): it is faster and far more forgiving than genfromtxt. See NumPy File IO and Reading and Writing Data.

The preallocation pattern

This is the single most valuable habit in this note.

# BAD: quadratic time, reallocates the whole buffer every iteration
result = np.array([])
for chunk in chunks:
    result = np.append(result, process(chunk))

# GOOD: preallocate when you know the size
result = np.empty(n_total, dtype=np.float32)
i = 0
for chunk in chunks:
    out = process(chunk)
    result[i:i + len(out)] = out
    i += len(out)

# ALSO GOOD: collect and convert once when you do not
pieces = [process(chunk) for chunk in chunks]
result = np.concatenate(pieces)

Arrays are fixed-size (ndarray); "appending" means allocating a new buffer and copying everything. On 10,000 iterations that is 10,000 copies of a growing array.

Bioinformatics examples

# a genotype matrix placeholder: samples × variants, -1 = missing
gt = np.full((n_samples, n_variants), -1, dtype=np.int8)

# per-base coverage vector for a chromosome
cov = np.zeros(chrom_length, dtype=np.int32)

# a position weight matrix scaffold: 4 bases × motif length
pwm = np.zeros((4, motif_len), dtype=np.float64)

# one-hot encoding of a sequence
BASES = np.array(list("ACGT"))
onehot = (np.array(list(seq))[:, None] == BASES[None, :]).astype(np.int8)

That last one is a nice Broadcasting exercise: an (L, 1) array of sequence letters compared against a (1, 4) array of bases gives an (L, 4) boolean matrix.

Common mistakes

  • np.arange with a float step. Use linspace.
  • Growing arrays in a loop. Preallocate or collect-then-concatenate.
  • np.empty left partially filled. Garbage that looks like data.
  • np.zeros(3, 4): passing dimensions as separate arguments. The shape must be a tuple: np.zeros((3, 4)). (np.zeros(3, 4) interprets 4 as the dtype and errors confusingly.)
  • Forgetting the default dtype is float64. np.zeros(5)[0] = 1 stores 1.0. If you wanted an integer index array, say so.

See also

ndarray · NumPy dtypes · NumPy Random Generator · NumPy File IO · Fancy Indexing · Reshaping and Stacking

Exercise

Write bin_report(lo, hi, nbins), which lays out a histogram over the interval [lo, hi] and reports on the two ways of constructing it. Return ["empty"] when nbins is less than one. Otherwise return exactly four lines.

The first line is edges=<list>, the nbins + 1 inclusive edges produced by np.linspace, each rounded to three decimals. The second is centers=<list>, the midpoint of each bin, rounded the same way. The third is arange_matches=<bool>, which is true only when np.arange(lo, hi, width) with width = (hi - lo) / nbins happens to produce exactly nbins values: with a float step it often does not, which is why linspace exists. The fourth is nbytes=<int>, the memory footprint of a counts array of nbins elements created with an explicit np.int16 dtype rather than the float64 default.

exerciseEvery submission runs independently in a fresh namespace.

Test cases · 4

#viainputexpected stdout
1stdin0 1 4edges=[0.0, 0.25, 0.5, 0.75, 1.0] centers=[0.125, 0.375, 0.625, 0.875] arange_matches=True nbytes=8
2stdin0 0.3 3edges=[0.0, 0.1, 0.2, 0.3] centers=[0.05, 0.15, 0.25] arange_matches=True nbytes=6
3stdin2.5 3.5 1edges=[2.5, 3.5] centers=[3.0] arange_matches=True nbytes=2
4stdin0 1 0empty

Hints · 2

01Hint

np.linspace(lo, hi, nbins + 1) gives inclusive edges; the centers are (edges[:-1] + edges[1:]) / 2.

02Hint

np.arange with a float step accumulates rounding error, so compare .size against nbins rather than assuming.