NumPy · lesson 3 of 13
Creating NumPy Arrays
Create NumPy arrays from lists, ranges, linspace, zeros, ones, full, eye and random generators, and see the float-step arange trap with real output.
By SkillByExample · Updated
From Python Lists
np.array() turns a list, or a nested list, into an array and infers one dtype for all of it.
import numpy as np
prices = np.array([10.5, 20.0, 15.75, 8.25])
matrix = np.array([[1, 2, 3], [4, 5, 6]])
mixed = np.array([1, 2.5, 3])
small = np.array([10.5, 20.0, 15.75], dtype=np.float32)
print(prices.dtype, matrix.shape, matrix.dtype)
print(mixed, mixed.dtype)
print(small.nbytes, "bytes as float32")
$ python from_lists.py
float64 (2, 3) int64
[1. 2.5 3. ] float64
12 bytes as float32
mixed contained one float, so NumPy upcast the whole array to float64 — an array has a
single dtype, and it picks one wide enough for every element. Passing dtype=np.float32
overrides inference: three 4-byte floats take 12 bytes instead of 24.
Ranges: arange and linspace
import numpy as np
print(np.arange(0, 10, 2))
print(np.linspace(0, 1000, 5))
print(np.logspace(-4, -1, 4))
$ python ranges.py
[0 2 4 6 8]
[ 0. 250. 500. 750. 1000.]
[0.0001 0.001 0.01 0.1 ]
arange works like range (stop excluded). linspace asks for a count of points and
includes both ends, which is what you want for plot axes and sample grids. logspace spaces
points evenly in powers of ten — a common grid for learning-rate searches.
The Float-Step arange Trap
import numpy as np
a = np.arange(1, 1.3, 0.1)
print(a, len(a))
print(np.linspace(1, 1.2, 3))
print((1.3 - 1) / 0.1)
$ python arange_trap.py
[1. 1.1 1.2 1.3] 4
[1. 1.1 1.2]
3.0000000000000004
The stop value 1.3 was supposed to be excluded, yet it appears. arange computes its length
as ceil((stop - start) / step), and the last line shows that division comes out a hair above
3 in floating point, so the ceiling is 4. linspace sidesteps the problem by taking the count
directly.
Pre-filled Arrays
import numpy as np
print(np.zeros((2, 3)))
print(np.ones((2, 3), dtype=np.int32))
print(np.full(4, 0.01))
print(np.eye(3))
$ python prefilled.py
[[0. 0. 0.]
[0. 0. 0.]]
[[1 1 1]
[1 1 1]]
[0.01 0.01 0.01 0.01]
[[1. 0. 0.]
[0. 1. 0.]
[0. 0. 1.]]
zeros and eye default to float64 (note the trailing dots), while dtype=np.int32 made
the ones array integer. Shapes are passed as tuples — (2, 3) — except for 1-D arrays,
where a bare integer works.
Uninitialized Memory with np.empty
import numpy as np
buf = np.empty(5)
print("before:", buf)
buf[:] = np.arange(5) * 1.5
print("after: ", buf)
$ python empty.py
before: [5.82554639e-313 2.00806763e+289 1.45694829e-308 9.21286186e-318
6.49172988e-309]
after: [0. 1.5 3. 4.5 6. ]
The “before” line is not guaranteed to be zeros or anything else — np.empty hands you memory
without clearing it, so its contents vary between runs and machines. It is only safe when every
element is overwritten before being read, as the slice assignment does here.
Arrays Shaped Like Another Array
import numpy as np
data = np.array([[1.5, 2.0], [3.0, 4.5]])
print(np.zeros_like(data))
print(np.full_like(data, 7))
print(np.ones_like(data, dtype=int))
$ python like.py
[[0. 0.]
[0. 0.]]
[[7. 7.]
[7. 7.]]
[[1 1]
[1 1]]
The _like functions copy the shape and dtype of an existing array, so full_like(data, 7)
produced floats (7.) because data is float. Pass dtype= to override, as the last line does.
Random Arrays
import numpy as np
rng = np.random.default_rng(seed=42)
print(rng.random(3))
print(rng.integers(0, 10, size=5))
signal = rng.normal(loc=5.0, scale=2.0, size=1000)
print(f"mean {signal.mean():.2f}, std {signal.std():.2f}")
print(rng.choice(np.arange(100), size=5, replace=False))
$ python random_arrays.py
[0.77395605 0.43887844 0.85859792]
[0 6 2 0 5]
mean 4.95, std 1.97
[31 99 93 65 12]
With a fixed seed these exact numbers come back on every run. The sample mean and standard deviation land near, not exactly on, the requested 5.0 and 2.0 — a thousand draws is a sample, not the distribution. The NumPy Random lesson covers generators in depth.
Diagonals
import numpy as np
print(np.diag([1, 2, 3]))
m = np.array([[10, 2, 3], [4, 50, 6], [7, 8, 90]])
print(np.diag(m))
$ python diag.py
[[1 0 0]
[0 2 0]
[0 0 3]]
[10 50 90]
np.diag works in both directions: given a 1-D input it builds a square matrix with those
values on the diagonal, and given a 2-D input it extracts the diagonal.
Practice
- Create 7 evenly spaced values from 0 to 3 inclusive, once with
linspaceand once witharange. Which call is easier to get right? - Build a 3 × 3 integer array filled with
-1. - Show that
np.arange(0, 0.3, 0.1)does not hit the trap. Print its length and explain.
Solution 1
import numpy as np
print(np.linspace(0, 3, 7))
print(np.arange(0, 3.5, 0.5))
$ python practice_seven.py
[0. 0.5 1. 1.5 2. 2.5 3. ]
[0. 0.5 1. 1.5 2. 2.5 3. ]
linspace takes the count directly. With arange you have to work out the step (0.5) and pick
a stop beyond 3 so that 3 is included.
Solution 2
import numpy as np
print(np.full((3, 3), -1))
$ python practice_full.py
[[-1 -1 -1]
[-1 -1 -1]
[-1 -1 -1]]
np.full infers an integer dtype from the fill value -1.
Solution 3
import numpy as np
a = np.arange(0, 0.3, 0.1)
print(a, len(a))
print((0.3 - 0) / 0.1)
$ python practice_notrap.py
[0. 0.1 0.2] 3
2.9999999999999996
Here the division rounds down to just under 3, so the ceiling is 3 and the stop is excluded
as intended. Whether the trap fires depends on which way the float division happens to round,
which is exactly why a float-step arange is unreliable.