Installing NumPy, the ndarray and Generating Arrays
Context
NumPy is a library whose central object, the ndarray, holds many values of one type side by side in one block of memory, with a shape recording how many rows and columns they form. You build one from a Python list or generate one by rule: a constant, an evenly spaced range, or random numbers from a generator whose starting point you fix.
Nearly all the numeric work in the rest of the bootcamp runs on these arrays or on objects built like them. The features of a set of housing listings, area, rooms and age, become a two-dimensional array with one row per listing and one column per feature, and the prices a model learns to predict become a one-dimensional array beside it. The models of Module 3 take such arrays as input, and a seed is what makes a synthetic test set or a train and test split come out the same on every run.
The failures here are quiet. One stray "unknown" in a list of build years turns the whole array into text, NumPy builds it without a word, and the error arrives later from a calculation that names types instead of the value. A range that ends one value away from where you meant shifts everything built on it, and a generator re-created with the same seed inside a loop hands every experiment the same numbers. Section 2.1 returned rows from a database as tuples, and this lesson installs the library that holds such values as arrays, in the (bootcamp) environment.
Goal
- Install NumPy 2.5.2 into
(bootcamp)withuv pip installand confirm the version from a script. - Build arrays from Python lists with
np.arrayand predict thedtypeNumPy infers, the silent switch to text included. - Read an array's
shape,ndim,sizeandlen, and state what each one counts. - Generate filled and ranged arrays with
np.zeros,np.ones,np.full,np.arangeandnp.linspace, choosing between the last two by whether the stop belongs in the result. - Produce reproducible random arrays from a seeded
np.random.default_rngGenerator. - Reshape an array with
reshape,-1andravel, and diagnose a reshape whose sizes do not multiply out.
Prerequisites
None of this is required, the lesson stands on its own. Both are here if you would like to see arrays being built before you build your own.
- YouTube - "Python NumPy Tutorial for Beginners" by Keith Galli, on freeCodeCamp.org. Its chapters "The Basics", from 11:08, and "Initializing Different Arrays", from 23:14, build arrays and read their shape and type, though its random numbers use the older style this lesson replaces.
- Official docs - NumPy, "the absolute basics for beginners", "How to create a basic array".
np.zeros,np.ones,np.arangeandnp.linspacein one short section, each with what it returns, anddtype=to choose the element type.
Table of Contents
- Installing NumPy
- The ndarray and its dtype
- Filled and Ranged Arrays
- Reproducible Randomness - the Generator
- Reshaping
Theory
Installing NumPy
What. NumPy is the third-party library for numeric arrays in Python, imported as numpy, and numpy==2.5.2 is the exact release every array in this module was printed by.
Why. (bootcamp) does not have it yet. Lesson 1.7.3 installed NumPy into the environment it built in ~/project, and an install belongs to the environment that received it, so import numpy in (bootcamp) still fails.
When. Once, now, with (bootcamp) active. The rest of the module uses NumPy from this environment.
With (bootcamp) active, install the pinned release with the command Module 2's setup page introduced.
uv pip install numpy==2.5.2uv prints a few progress lines, and the last one names what changed in the environment:
+ numpy==2.5.2Until the install ran, (bootcamp) answered an import of NumPy with the failure Lesson 1.7.3's first lab recorded:
(bootcamp) goku@machine:~/dev/bootcamp$ python3 -c "import numpy"
Traceback (most recent call last):
File "<string>", line 1, in <module>
ModuleNotFoundError: No module named 'numpy'Confirm it from a script, check_numpy.py, with the np alias Lesson 1.7.3 introduced:
import numpy as np
print(np.__version__) # 2.5.2np.__version__ reports the NumPy the import actually found, which is the one your arrays come from, not the one you meant to install. 2.5.2 means every array in this module prints as the lessons quote it.
DON'T:
- DON'T run the install a second time to check that it worked. A second run finds 2.5.2 already in place and prints no
+ numpy==2.5.2line at all, andnp.__version__from a script is the check. - DON'T save a script of your own as
numpy.pyin~/dev/bootcamp. The folder of the script you run comes first on the search path, soimport numpyloads your file instead of the library, and the first NumPy name used after it fails with anAttributeErrorsuch asmodule 'numpy' has no attribute '__version__', or, when your file itself imports numpy,partially initialized module 'numpy' has no attribute 'array' (most likely due to a circular import).
The ndarray and its dtype
What. An ndarray is a grid of values that all share one type, its dtype, stored side by side in one block of memory. np.array builds one from a list, or from a list of equal-length lists for two dimensions.
Why. One type for every element is what lets NumPy run a single operation across the whole block in compiled code, and it is what keeps a column of prices a column of numbers. The dtype is decided once, when the array is built, so one wrong element decides it for all of them.
When. Build an array as soon as a column of data is numeric and you mean to compute on it, and read its dtype and shape right after you build it.
np.array reads the list and infers one dtype that can hold every element. Whole numbers give int64, a 64-bit integer, and a list with decimals gives float64, a 64-bit floating-point number. An array prints without commas, and a float array writes 110. for 110.0, padding every value to one width.
import numpy as np
# one asking price per listing, whole numbers
prices = np.array([310000, 455000, 289000, 520000])
area_sqm = np.array([72.5, 110.0, 64.0, 135.5])
print(type(prices)) # <class 'numpy.ndarray'>
print(prices) # [310000 455000 289000 520000]
print(prices.dtype) # int64
print(area_sqm) # [ 72.5 110. 64. 135.5]
print(area_sqm.dtype) # float64A list that mixes types still produces one dtype, one that can hold every element. One decimal among whole numbers makes every element a float. One string among numbers makes every element a string, and nothing warns you.
import numpy as np
rooms = np.array([3, 4, 2.5]) # one listing counts a half room
print(rooms) # [3. 4. 2.5]
print(rooms.dtype) # float64
# one missing year, typed as text by whoever filled the sheet
year_built = np.array([1998, 2005, "unknown", 2016])
print(year_built) # ['1998' '2005' 'unknown' '2016']
print(year_built.dtype) # <U21<U21 is NumPy's text dtype: U for Unicode text, 21 for the longest string an element can hold, and < for the byte order, which you can ignore here. The quotes in the printed array are the only visible sign that 1998 is now the text '1998', and a calculation on this column fails later with an error that names the types involved, not the 'unknown' that caused it.
When you know the type a column should have, say so at creation with dtype=, and NumPy converts each value as it builds the array. float there means float64.
import numpy as np
# whole-number prices, stored as floats from the start
prices = np.array([310000, 455000, 289000], dtype=float)
print(prices) # [310000. 455000. 289000.]
print(prices.dtype) # float64A list of equal-length lists builds a two-dimensional array, one inner list per row. Three attributes and one built-in function describe its layout: shape, a tuple with the length of each dimension, rows first, ndim, the number of dimensions, size, the total number of elements, and Python's len, which counts only the first dimension, the rows.
import numpy as np
# one row per listing: area_sqm, rooms, age in years
features = np.array([
[72.5, 3, 26],
[110.0, 4, 19],
[64.0, 2, 49],
[135.5, 5, 8],
])
print(features)
print(features.shape, features.ndim, features.size, len(features))
print(features.dtype)[[ 72.5 3. 26. ]
[110. 4. 19. ]
[ 64. 2. 49. ]
[135.5 5. 8. ]]
(4, 3) 2 12 4
float64The room counts and ages print as 3. and 26. because one dtype covers the whole table, and the areas made it float64. A 1-D array has a one-element shape tuple, so area_sqm.shape is (4,), with the trailing comma that marks a tuple of one.
astype converts an array to another dtype and returns a new array, leaving the original as it was. A finite float converted to an integer loses its fractional part, which is not the same as rounding.
import numpy as np
area_sqm = np.array([72.5, 110.9, 64.0, 135.5])
whole_sqm = area_sqm.astype(int) # a NEW int64 array
print(whole_sqm) # [ 72 110 64 135]
print(area_sqm) # [ 72.5 110.9 64. 135.5]
year_text = np.array(["1998", "2005", "2016"])
print(year_text.astype(int)) # [1998 2005 2016]110.9 became 110, not 111. Text converts to integers when every element reads as a whole number, with no decimal point, and that makes astype the line that exposes a stray string, because it fails at the first element that does not. Saved as years.py:
import numpy as np
year_built = np.array([1998, 2005, "unknown", 2016])
year_built.astype(int)Traceback (most recent call last):
File "/home/goku/dev/bootcamp/years.py", line 4, in <module>
year_built.astype(int)
ValueError: invalid literal for int() with base 10: np.str_('unknown')The message names the culprit, 'unknown', which is more than the array itself ever said. np.str_ is how NumPy labels one text element taken out of a text array.
DON'T:
- DON'T build a numeric array from values you have not checked. One stray string such as
"unknown", in a list given nodtype=, turns every element into text, adtypesuch as<U21, and NumPy builds the array without raising anything. - DON'T count elements with
len. On a 2-D array it returns the number of rows,4forfeatures, wheresizereturns all12values. - DON'T expect
astype(int)to round. It drops the fractional part, so110.9becomes110and-2.9becomes-2.
Filled and Ranged Arrays
What. np.zeros, np.ones and np.full build an array of a given shape with every element set to one value. np.arange and np.linspace build a 1-D array of evenly spaced numbers.
Why. Most arrays are not typed out by hand. A shape filled with zeros is where results get written, a constant array is a default value, and an evenly spaced sequence is a set of years, thresholds or candidate settings to try.
When. Use a filled array when you know the shape before the values, np.arange when you know the step, and np.linspace when you know how many points you want between two ends.
The shape is the first argument, an integer for a 1-D array. np.zeros and np.ones fill with float64 unless you pass dtype=, and np.full takes its dtype from the fill value.
import numpy as np
predicted = np.zeros(4) # slots for four model predictions
print(predicted) # [0. 0. 0. 0.]
print(predicted.dtype) # float64
default_rooms = np.full(4, 3) # a default of 3 rooms per listing
print(default_rooms) # [3 3 3 3]
print(default_rooms.dtype) # int64A 2-D shape is one argument as well, a tuple of rows and columns.
import numpy as np
# 2 rows, 3 columns, passed as ONE tuple
weights = np.ones((2, 3))
print(weights)[[1. 1. 1.]
[1. 1. 1.]]Leave the tuple's parentheses off and the second number lands in the next parameter, which is dtype. Saved as placeholder.py:
import numpy as np
grid = np.zeros(3, 2) # meant 3 rows by 2 columnsTraceback (most recent call last):
File "/home/goku/dev/bootcamp/placeholder.py", line 3, in <module>
grid = np.zeros(3, 2) # meant 3 rows by 2 columns
^^^^^^^^^^^^^^
TypeError: Cannot interpret '2' as a data typenp.arange(start, stop, step) is the array version of range, and it stops before stop, exactly as range does. np.linspace(start, stop, num) returns num evenly spaced values and includes stop by default, so its third argument is a count, never a step.
import numpy as np
years = np.arange(2000, 2025, 5) # stops BEFORE 2025
print(years) # [2000 2005 2010 2015 2020]
thresholds = np.linspace(0, 1, 5) # 5 points, both ends included
print(thresholds) # [0. 0.25 0.5 0.75 1. ]A float step is where np.arange stops being predictable. It decides how many values to return by rounding (stop - start) / step up to a whole number, and a decimal step is stored only approximately, so that division can land a hair above the count you meant. Three train fractions, the share of rows a model trains on, look like a one-liner:
import numpy as np
# meant: 0.7, 0.8 and 0.9
train_fractions = np.arange(0.7, 1.0, 0.1)
print(train_fractions) # [0.7 0.8 0.9 1. ]
print(len(train_fractions)) # 4
print((1.0 - 0.7) / 0.1) # 3.0000000000000004
safe_fractions = np.linspace(0.7, 0.9, 3)
print(safe_fractions) # [0.7 0.8 0.9](1.0 - 0.7) / 0.1 comes out a hair above 3, so np.arange rounds the count up to 4 and adds a fourth value, 0.9999999999999999, which the array prints as 1., a train fraction that leaves next to nothing to test on. np.linspace takes the count from you, so it cannot drift.
DON'T:
- DON'T pass a 2-D shape as two arguments.
np.zeros(3, 2)reads the2as adtypeand raisesTypeError, whilenp.zeros((3, 2))is the 3 by 2 array. - DON'T expect
np.arangeto include its stop.np.arange(2000, 2025, 5)ends at2020, andnp.linspace(2000, 2025, 6)is a call that ends at2025, as floats. - DON'T use a float step when the count matters.
np.arange(0.7, 1.0, 0.1)returns four values, the last shown as1.in the printed array, andnp.linspacewith the count you want returns exactly that many.
Reproducible Randomness - the Generator
What. A Generator is an object that produces random numbers, built with np.random.default_rng(seed). The seed fixes where its sequence starts, so a Generator built with the same seed produces the same numbers in the same order.
Why. Random draws build synthetic data, shuffle rows and split a dataset into training and test parts, and a result that changes on every run cannot be checked by anyone, you tomorrow included. A seed makes random repeatable.
When. Build one Generator, seeded, near the top of a script, and draw every random number from it.
rng.integers(low, high, size=n) returns whole numbers from low up to, but not including, high, like range. rng.normal(loc, scale, size=n) draws from a normal distribution, the bell-shaped spread of values around a center loc, where scale sets how far they typically stray (the standard deviation, which Lesson 2.2.5 computes). rng.uniform(low, high, size=n) returns decimals spread evenly from low up to high.
import numpy as np
rng = np.random.default_rng(42) # one Generator, seed 42
rooms = rng.integers(1, 6, size=5) # 1 to 5, never 6
area_sqm = rng.normal(90, 20, size=4) # centered on 90
age = rng.uniform(0, 60, size=4) # evenly spread, 0 to 60
print(rooms) # [1 4 4 3 3]
print(area_sqm) # [108.81129433 50.97929623 63.95640986 92.55680806]
print(age) # [47.16385832 7.68681796 27.02315627 22.24788145]Run it again and every number comes back identical. Within one script, a Generator keeps moving through its sequence, so a second draw continues where the first stopped instead of repeating it, while a fresh Generator with the same seed starts again from the beginning. np.array_equal(a, b) returns True when two arrays have the same shape and the same values, the check to write once arrays are too long to compare by eye.
import numpy as np
rng = np.random.default_rng(42)
print(rng.integers(1, 6, size=5)) # [1 4 4 3 3]
print(rng.integers(1, 6, size=5)) # [5 1 4 2 1]
again = np.random.default_rng(42) # a FRESH Generator, same seed
print(again.integers(1, 6, size=5)) # [1 4 4 3 3]
first = np.random.default_rng(42).integers(1, 6, size=5)
second = np.random.default_rng(42).integers(1, 6, size=5)
print(np.array_equal(first, second)) # TrueTutorials written before the Generator existed call np.random.seed(42) and then functions such as np.random.randint. That seeds one hidden generator shared by the whole program, from NumPy's older design, and a Generator never reads it. The two styles give different numbers for the same seed, and a np.random.default_rng() built without a seed of its own changes on every run, whatever np.random.seed was given.
import numpy as np
np.random.seed(42) # the older style
print(np.random.randint(1, 6, size=5)) # [4 5 3 5 5]
rng = np.random.default_rng(42) # the Generator style
print(rng.integers(1, 6, size=5)) # [1 4 4 3 3]DON'T:
- DON'T build a seeded Generator inside a loop. Each pass restarts the same sequence, so
np.random.default_rng(5).integers(0, 100, size=3)returns[67 80 2]on every pass. - DON'T read
rng.integers(1, 6)as including6. The high end is excluded, so room counts up to 6 needrng.integers(1, 7). - DON'T mix
np.random.seedwith a Generator. The seed reaches only the older shared generator, and a Generator built without its own seed still changes on every run.
Reshaping
What. reshape returns the same values arranged in a new shape, filled row by row in the order they sit, and ravel flattens them back into one dimension.
Why. Values often arrive in the wrong layout, a flat run of numbers that is really four listings of three features each, or a table that the next step needs as one long line. Reshaping changes the layout and never a value.
When. Reshape when the values are right but the shape is not the one the next step needs.
The sizes of the new shape must multiply out to the array's size, here 4 times 3 for 12 values.
import numpy as np
# area_sqm, rooms and age of four listings, one after another
flat = np.array([72.5, 3, 26, 110.0, 4, 19, 64.0, 2, 49, 135.5, 5, 8])
features = flat.reshape(4, 3) # 4 rows of 3, filled row by row
print(flat.shape)
print(features.shape)
print(features)(12,)
(4, 3)
[[ 72.5 3. 26. ]
[110. 4. 19. ]
[ 64. 2. 49. ]
[135.5 5. 8. ]]A -1 in place of one size tells NumPy to work that size out from the others, which is how you say "rows of 3, however many that makes". ravel goes the other way.
import numpy as np
flat = np.array([72.5, 3, 26, 110.0, 4, 19, 64.0, 2, 49, 135.5, 5, 8])
features = flat.reshape(-1, 3) # 12 values / 3 per row = 4 rows
print(features.shape) # (4, 3)
back = features.ravel() # one dimension again, row by row
print(back.shape) # (12,)Ask for a shape the values cannot fill and NumPy refuses instead of dropping or inventing values. Saved as reshape_error.py:
import numpy as np
flat = np.array([72.5, 3, 26, 110.0, 4, 19, 64.0, 2, 49, 135.5, 5, 8])
features = flat.reshape(5, 3)Traceback (most recent call last):
File "/home/goku/dev/bootcamp/reshape_error.py", line 4, in <module>
features = flat.reshape(5, 3)
^^^^^^^^^^^^^^^^^^
ValueError: cannot reshape array of size 12 into shape (5,3)When it can, reshape returns a view, a second array that reads and writes the same memory as the one it came from, and Lesson 2.2.2 shows why that matters the moment you write into one.
DON'T:
- DON'T reshape to a shape whose sizes do not multiply to the array's
size.flat.reshape(5, 3)needs 15 values and raisesValueErroron 12. - DON'T write
-1in two places. NumPy can work out one size, andflat.reshape(-1, -1)raisesValueError: can only specify one unknown dimension. - DON'T reshape values stored feature by feature, every area and then every room count, into rows of listings.
reshapefills row by row in the order the values sit, soreshape(4, 3)on four areas, four room counts and four ages puts the first three areas,72.5,110.0and64.0, into the first listing's row.
Lab Exercises
Lab Exercise 1: Profile a Listing Export, and Find the Column That Is Not Numbers
Five listings arrive as Python lists, exported from a spreadsheet:
prices = [310000, 455000, 289000, 520000, 398000]
area_sqm = [72.5, 110.0, 64.0, 135.5, 98.0]
rooms = [3, 4, 2, 5, 4]
year_built = [1998, 2005, "n/a", 2016, 1987]In a script listing_profile.py, print np.__version__ first. Then turn each list into an array, print its name, dtype, shape and size on one line, and print the year_built array itself. Add a comment naming the array that is not numeric and the two signs of it in what you printed.
Convert that array to integers with astype inside a try, and print the error. The sheet's owner then sends the export again with every year written as text, the missing one filled in as "1991". Convert that version to integers and print it.
Finally, build a feature table with one row per listing and three columns, area_sqm, rooms and age, using the ages [28, 21, 35, 10, 39]. Use zip to turn the three lists into rows. Print the table, then its shape, ndim, size, len and dtype, and say in a comment why the room counts no longer print as whole numbers.
Lab Exercise 2: A Seeded Synthetic Batch, Laid Out as a Table
Build every array below in code.
With one Generator seeded 2026, draw six room counts from 1 to 5 inclusive, six areas from a normal distribution centered on 85 with spread 15, and six ages spread evenly from 0 up to 50. Print each draw.
Build a second Generator with the same seed, draw the room counts again, and print whether that draw equals the first one. Then draw room counts once more from the first Generator, print whether this draw equals the first one, and add a comment explaining the two answers.
Build the decades 1980 to 2020 inclusive with np.arange, and five price thresholds from 200000 to 600000 inclusive with np.linspace.
Finally, the twelve numbers below are area_sqm, rooms and age of four listings, one listing after another. Turn them into a table with one row per listing, letting NumPy work out the number of rows, print the table and its shape, then flatten it back and print its shape.
values = [88.0, 3, 31, 142.5, 5, 6, 59.5, 2, 44, 101.0, 4, 17]Solution 1: Profile a Listing Export, and Find the Column That Is Not Numbers
import numpy as np
print(np.__version__)
prices = np.array([310000, 455000, 289000, 520000, 398000])
area_sqm = np.array([72.5, 110.0, 64.0, 135.5, 98.0])
rooms = np.array([3, 4, 2, 5, 4])
year_built = np.array([1998, 2005, "n/a", 2016, 1987])
print("prices", prices.dtype, prices.shape, prices.size)
print("area_sqm", area_sqm.dtype, area_sqm.shape, area_sqm.size)
print("rooms", rooms.dtype, rooms.shape, rooms.size)
print("year_built", year_built.dtype, year_built.shape, year_built.size)
print(year_built)
# year_built is text: its dtype is <U21, a text type, and every
# year prints inside quotes.
try:
year_built.astype(int)
except ValueError as err:
print("ValueError:", err)
year_text = np.array(["1998", "2005", "1991", "2016", "1987"])
print(year_text.astype(int))2.5.2
prices int64 (5,) 5
area_sqm float64 (5,) 5
rooms int64 (5,) 5
year_built <U21 (5,) 5
['1998' '2005' 'n/a' '2016' '1987']
ValueError: invalid literal for int() with base 10: np.str_('n/a')
[1998 2005 1991 2016 1987]import numpy as np
area_sqm = [72.5, 110.0, 64.0, 135.5, 98.0]
rooms = [3, 4, 2, 5, 4]
age = [28, 21, 35, 10, 39]
# zip pairs the i-th area, room count and age into one row
features = np.array(list(zip(area_sqm, rooms, age)))
print(features)
print(features.shape, features.ndim, features.size, len(features))
print(features.dtype)
# One dtype covers the table, and the areas need float64, so
# the room counts and ages are stored, and printed, as floats.[[ 72.5 3. 28. ]
[110. 4. 21. ]
[ 64. 2. 35. ]
[135.5 5. 10. ]
[ 98. 4. 39. ]]
(5, 3) 2 15 5
float64DON'T:
- DON'T read a
<U21column as numbers with quotes around them. Every element is text, and text sorts character by character, so the lab's areas written as text would sort'110.0'before'64.0', and a conversion is the only way back to numbers. - DON'T pass
zip(...)tonp.arraywithoutlist.np.array(zip(...))builds a 0-dimensional array ofdtypeobjectholding the zip object itself, not a table.
Solution 2: A Seeded Synthetic Batch, Laid Out as a Table
import numpy as np
rng = np.random.default_rng(2026)
rooms = rng.integers(1, 6, size=6) # high end 6 is excluded
area_sqm = rng.normal(85, 15, size=6)
age = rng.uniform(0, 50, size=6)
print(rooms)
print(area_sqm)
print(age)
again = np.random.default_rng(2026)
rooms_again = again.integers(1, 6, size=6)
print(np.array_equal(rooms, rooms_again))
rooms_next = rng.integers(1, 6, size=6)
print(np.array_equal(rooms, rooms_next))
# 'again' started its sequence over from seed 2026, so its first
# draw repeats. 'rng' had moved on past three draws, so it did not.[5 1 1 4 2 3]
[105.93657565 94.57442111 80.61928772 80.32076008 89.55753052
80.98509547]
[14.91513837 48.34811001 45.99250803 31.79354021 37.63660485 25.75768482]
True
Falseimport numpy as np
decades = np.arange(1980, 2030, 10) # stop one step past 2020
print(decades) # [1980 1990 2000 2010 2020]
thresholds = np.linspace(200000, 600000, 5) # 5 points, both ends
print(thresholds) # [200000. 300000. 400000. 500000. 600000.]import numpy as np
values = [88.0, 3, 31, 142.5, 5, 6, 59.5, 2, 44, 101.0, 4, 17]
table = np.array(values).reshape(-1, 3) # 12 / 3 = 4 rows
print(table)
print(table.shape)
print(table.ravel().shape)[[ 88. 3. 31. ]
[142.5 5. 6. ]
[ 59.5 2. 44. ]
[101. 4. 17. ]]
(4, 3)
(12,)DON'T:
- DON'T compare a Generator's later draw with its first and expect equality. Only a fresh Generator with the same seed starts over.
- DON'T stop
np.arangeat2020when2020must be in the result. The stop is excluded, so the stop goes one step past the last value you want.
Theory Assessment
Q1: One Type for Every Element
State the dtype NumPy gives each of np.array([3, 4, 2]), np.array([3, 4, 2.5]) and np.array([3, 4, "n/a"]), and why each gets it. Then explain why the third case is worse than an error would have been at the moment the array was built, and what astype(int) does when you call it on that third array.
Hint: An array has exactly one dtype, so ask which single type can hold every element of each list. For the last part, ask what converting "n/a" to an integer could possibly produce.
Q2: Where a Range Stops, and Which Function to Use
Write one call for each of these, saying why you chose np.arange or np.linspace: the years 2000 to 2025 inclusive in steps of 5, as integers, and exactly eleven evenly spaced thresholds from 0 to 1, both ends included.
Hint: One of the two functions excludes its stop and takes a step, the other includes its stop and takes a count.
Q3: The Experiments That Never Changed
A teammate writes this helper and calls it once per experiment, three experiments in a row, expecting a fresh batch of synthetic room counts each time:
import numpy as np
def sample_rooms(n):
rng = np.random.default_rng(0)
return rng.integers(1, 6, size=n)
for experiment in range(3):
print(sample_rooms(4))Say what the three experiments receive and why. Then rewrite the helper so that the three batches differ while the whole script still reproduces exactly on every run.
Hint: Count how many Generators this script builds and where each one starts its sequence. The fix moves one line and adds one parameter.
Q4: Four Prints, Four Surprises
Read this without running it, then state exactly what each print shows.
import numpy as np
steps = np.arange(0, 1, 0.2)
points = np.linspace(0, 1, 6)
grid = points.reshape(2, -1)
labels = np.array([1, 2, "3"])
print(len(steps), len(points))
print(grid.shape, len(grid), grid.size)
print(grid.astype(int))
print(labels.dtype, labels.astype(int))Hint: Work out every value of steps and points first, and remember which of the two functions includes its stop. Then ask what astype(int) does to 0.8, and what it does to a string that reads as a whole number.
Answer 1:
np.array([3, 4, 2]) is int64, because every element is a whole number. np.array([3, 4, 2.5]) is float64, because a float can hold 3, 4 and 2.5 and an integer cannot hold 2.5. np.array([3, 4, "n/a"]) is <U21, text, because a string type can hold "n/a" and no number type can, so 3 and 4 become the text '3' and '4'.
The third case is worse than an error because there is none. The array builds and prints looking almost like numbers, and the failure surfaces later, in a calculation whose error names types rather than the value that caused it.
astype(int) tries to convert every element and stops at the first one that does not read as a whole number with no decimal point, raising ValueError: invalid literal for int() with base 10: np.str_('n/a'). That message names the culprit, which is why astype is the line to write right after you build a column you expect to be numeric.
DON'T trust a numeric-looking print of a column, check its dtype, because a text array prints the same digits with quotes around them.
Answer 2:
The years are np.arange(2000, 2030, 5): the step is known, and the stop is excluded, so it goes one step past 2025. np.linspace(2000, 2025, 6) reaches the same years as floats, which the question rules out. The thresholds are np.linspace(0, 1, 11): the count is known and both ends belong in the result, and a float step in np.arange could not promise eleven values.
import numpy as np
print(np.arange(2000, 2030, 5)) # [2000 2005 2010 2015 2020 2025]
print(np.linspace(0, 1, 11)) # [0. 0.1 0.2 0.3 0.4 0.5 0.6 0.7 0.8 0.9 1. ]DON'T pick between the two by habit. Ask whether you know the step or the count, and whether the stop belongs in the result.
Answer 3:
All three experiments receive the identical batch, [5 4 3 2]. Every call builds a brand-new Generator seeded with 0, and a fresh Generator starts its sequence from the beginning, so each call hands back the sequence's first four values.
[5 4 3 2]
[5 4 3 2]
[5 4 3 2]The fix is to build the Generator once, outside the helper, and pass it in, so each call continues the sequence where the last one stopped:
import numpy as np
def sample_rooms(rng, n):
return rng.integers(1, 6, size=n)
rng = np.random.default_rng(0) # built ONCE, seeded once
for experiment in range(3):
print(sample_rooms(rng, 4))[5 4 3 2]
[2 1 1 1]
[1 5 4 5]The three batches differ, and running the script again prints the same three batches, because the one Generator starts from the same seed every run.
DON'T seed inside a function that is called repeatedly. The seed belongs where the run starts, once.
Answer 4:
5 6
(2, 3) 2 6
[[0 0 0]
[0 0 1]]
<U21 [1 2 3]steps is [0. 0.2 0.4 0.6 0.8], five values, because np.arange excludes its stop and 1 is the stop. points is [0. 0.2 0.4 0.6 0.8 1. ], six values, because np.linspace returns exactly the count you ask for and includes the stop.
points.reshape(2, -1) makes two rows and works out three columns. len(grid) counts the first dimension only, the 2 rows, and grid.size counts all 6 values.
grid.astype(int) drops every fractional part, so 0.2, 0.4, 0.6 and 0.8 all become 0, and only 1.0 becomes 1. A reader expecting rounding predicts two 1s too many.
labels is <U21, because the single string "3" turned the 1 and the 2 into text as well. astype(int) then succeeds and returns [1 2 3], because every element reads as a whole number. The string was silent when the array was built, and the conversion fails only on an element that does not read as a whole number with no decimal point, which this time none did.
DON'T use astype(int) as a rounding step. It truncates, and the error it can raise is about text, never about precision.
Assignment
Assignment: A Seeded Listing Generator That Survives a New Column
You are building a small synthetic dataset to test a cleaning script before the real listings arrive, and other people's tests will compare their results against yours, so the same seed must always produce the same table.
Write make_listings(n, seed). From one Generator seeded with seed it draws n areas from a normal distribution centered on 90 with spread 25, then n room counts from 1 to 6 inclusive, then n ages spread evenly from 0 up to 60, and it returns one table with a row per listing and the three features as columns. np.column_stack([a, b, c]) builds that table: it takes a list of equal-length 1-D arrays and returns a 2-D array whose columns are those arrays, in the order given. Show that two calls with seed 11 and n=5 give equal tables and a call with seed 12 does not, print the seed-11 table and its dtype, and explain in a comment why the room counts print as floats.
A colleague then asks for a neighborhood label per listing. rng.choice(options, size=n) picks n items from the list options, independently each time, so a label can repeat. Extend the function to return the labels from ["Downtown", "Riverside", "Hillcrest"] alongside the table, without changing the seed-11 table that existing tests depend on, and prove it unchanged. Last, stack the labels into the table as a fourth column in an array mixed, print its dtype and its first row, mixed[0], which indexes a 2-D array the way a list's first item is indexed, attempt astype(float) on it, and explain why the function returns the labels as a separate array.
Hints:
- A Generator hands out one sequence in call order, so where the new draw goes in the function decides whether any existing value moves.
rng.choiceon a list of strings returns a text array, and one array holds exactly onedtype.
Solution:
The script keeps the first version as make_table, to compare against, and adds a variant that draws the labels first, to show why the order matters.
import numpy as np
OPTIONS = ["Downtown", "Riverside", "Hillcrest"]
def make_table(n, seed):
# the first version: the three features only
rng = np.random.default_rng(seed)
area_sqm = rng.normal(90, 25, size=n)
rooms = rng.integers(1, 7, size=n) # the 7 is excluded
age = rng.uniform(0, 60, size=n)
return np.column_stack([area_sqm, rooms, age])
def make_listings(n, seed):
rng = np.random.default_rng(seed)
area_sqm = rng.normal(90, 25, size=n)
rooms = rng.integers(1, 7, size=n)
age = rng.uniform(0, 60, size=n)
labels = rng.choice(OPTIONS, size=n) # drawn LAST
return np.column_stack([area_sqm, rooms, age]), labels
def labels_drawn_first(n, seed):
rng = np.random.default_rng(seed)
labels = rng.choice(OPTIONS, size=n) # drawn FIRST
area_sqm = rng.normal(90, 25, size=n)
rooms = rng.integers(1, 7, size=n)
age = rng.uniform(0, 60, size=n)
return np.column_stack([area_sqm, rooms, age]), labels
before = make_table(5, 11)
print(np.array_equal(before, make_table(5, 11)))
print(np.array_equal(before, make_table(5, 12)))
print(before)
print(before.dtype)
# One table, one dtype: the areas and ages are floats, so the
# integer room counts were converted to float64 when stacked.
table, labels = make_listings(5, 11)
shifted, _ = labels_drawn_first(5, 11)
print(np.array_equal(before, table))
print(np.array_equal(before, shifted))
print(labels)
mixed = np.column_stack([table, labels])
print(mixed.dtype)
print(mixed[0])
try:
mixed.astype(float)
except ValueError as err:
print("ValueError:", err)True
False
[[ 90.85481918 3. 56.8997072 ]
[123.99368851 6. 37.31301557]
[120.61802696 4. 22.13958742]
[ 77.24232308 1. 30.68340131]
[ 82.55076222 4. 39.77057715]]
float64
True
False
['Downtown' 'Hillcrest' 'Downtown' 'Hillcrest' 'Downtown']
<U32
['90.8548191813296' '3.0' '56.8997071975065' 'Downtown']
ValueError: could not convert string to float: np.str_('Downtown')Drawing the labels last leaves the first three draws exactly where they were, so the seed-11 table survives the new column. Drawing them first uses up the start of the sequence, the features come from a later part of it, and every test built on the old table breaks without an error to say why.
The one dtype NumPy chooses for 'Downtown' beside numbers is text, so every area, room count and age in mixed became a string, and it cannot be converted back as a whole because the label column never reads as a number. That is why make_listings returns two arrays, a float64 table and a text array of labels, each with the dtype its values need. The table library of Section 2.4 makes the same choice per column, inside one object.
DON'T add a new random draw ahead of existing ones in a seeded script that other people's results depend on, it silently moves every value drawn after it, and don't stack text and numbers into one array, the numbers become text.
Additional Resources
Optional, and nothing later in the bootcamp depends on either. Worth opening if you want to know more about choosing a seed, or about the builders for special matrices.
- Official docs - NumPy, "Random sampling", "Quick start". Why a Generator built without a seed differs on every run, and why the docs recommend a large seed drawn once with
secrets.randbitswhen your stream must not collide with anyone else's. - Official docs - NumPy, "Array creation", "Intrinsic NumPy array creation functions". The builders for special 2-D arrays,
np.eyefor an identity matrix,np.diagandnp.vander, beside thenp.arangeandnp.linspaceyou now know.
Summary
An array looks like a list with the commas taken out, and the resemblance stops there. A list holds any mix of types, and an array holds one type, decided once, for every element it will ever contain. That decision is what lets NumPy compute quickly, and it is also why the most dangerous line in this lesson is a list with one stray string in it: the array it builds is valid, it is simply no longer numbers, and nothing complains. Reading dtype and shape the moment an array exists is the habit that turns that silence into something you notice.
Most arrays are generated rather than typed, and the generators carry decisions of their own. np.arange and np.linspace differ by one argument and one included endpoint, which is exactly the size of the off-by-one errors they produce. A seeded Generator turns randomness into something you can hand to someone else and get the same numbers back, the precondition for comparing two runs at all, so a seed is part of a script's result rather than a detail of it.
Reshaping closes on the idea the next lesson opens. The same twelve numbers are a flat record or a four-by-three table, and only the way they are addressed changes. Lesson 2.2.2 starts addressing them, a row, a column, a filtered selection, and some of those selections turn out to be windows onto the original memory rather than copies of it, which decides whether an edit stays where you made it.
Cheat Sheet
Part A - Reference Table
| Task | Code | Returns | Watch for |
|---|---|---|---|
| Install | uv pip install numpy==2.5.2 |
+ numpy==2.5.2 |
(bootcamp) active, it has no pip |
| From a list | np.array([1, 2, 3]) |
int64 array |
one string makes every element text |
| Force a type | np.array(x, dtype=float) |
float64 array |
set at build time |
| Layout | a.shape, a.ndim, a.size, len(a) |
tuple, int, int, int | len counts rows only |
| Convert | a.astype(int) |
new array | truncates finite floats, raises on text that is not a whole number with no decimal point |
| Filled | np.zeros(n), np.ones((r, c)), np.full(n, v) |
new array | shape is ONE argument |
| Stepped range | np.arange(start, stop, step) |
stop excluded | float steps miscount |
| Counted range | np.linspace(start, stop, num) |
stop included | third argument is a count |
| Generator | rng = np.random.default_rng(seed) |
a Generator | build once, never in a loop |
| Draws | rng.integers(lo, hi, size=n), rng.normal(loc, scale, size=n), rng.uniform(lo, hi, size=n) |
new arrays | hi excluded |
| Compare | np.array_equal(a, b) |
True or False |
needs same shape and values |
| Reshape | a.reshape(r, c), a.reshape(-1, c), a.ravel() |
new shape, filled row by row | sizes must multiply to a.size |
Part B - Compact Code Block
import numpy as np
prices = np.array([310000, 455000, 289000]) # int64
# float64, (2, 3)
X = np.array([[72.5, 3, 26], [110.0, 4, 19]])
X.shape, X.ndim, X.size, len(X) # (2, 3), 2, 6, 2
X.astype(int) # truncates, new array
np.zeros(4) # float64 zeros
np.ones((2, 3)) # shape as ONE tuple
np.full(4, 3) # int64, from the fill
np.arange(2000, 2025, 5) # stop excluded
np.linspace(0, 1, 5) # 5 points, stop included
rng = np.random.default_rng(42) # seed once
rng.integers(1, 6, size=5) # 1 to 5
rng.normal(90, 20, size=4) # around 90
rng.uniform(0, 60, size=4) # 0 up to 60
flat = np.arange(12)
flat.reshape(-1, 3) # (4, 3), rows worked out
flat.reshape(4, 3).ravel() # back to (12,)