Skip to content
  • Home
  • How To
  • M0
  • M1
  • Agents
  • Module 2 - Data
  • M3
  • M4
  • M5
  • M6
  • Progress
  • Contact
  • Pricing
  • Sign in
  • Future
  1. Module 2M2
  2. ›
    S3Linear Algebra, Computed
  3. ›
    Lesson 1 of 5Vectors and the Dot Product
  4. ›
    S3›Lesson 1 of 5
  1. Module 2
  2. ›
  3. Section 3
  4. ›
  5. Lesson 1

Vectors and the Dot Product

FREE·~1.5 h·2 labs·4 questions·1 assignment


Context

A vector is an ordered list of numbers, and a listing's features written in a fixed order make one, each position standing for one feature. The dot product takes two vectors of the same length, multiplies them position by position and adds the products, and the single number it returns, divided by the two vectors' lengths (how far each one's point sits from the all-zero point, not how many entries it has), measures how closely they point the same way.

A linear price model prices a listing with one dot product, its weights against the listing's features plus a fixed amount, and Module 3 fits those weights from data. Finding the listings whose profile is most like the one a buyer is looking at compares directions with the same operation divided by two lengths. Every matrix product, starting with the next lesson's, is a table of dot products.

Vectors that disagree about the order of the features price a listing without an error or a warning, and the price comes out wrong by hundreds of thousands. Writing the element-wise product where the dot product was meant returns one product per feature instead of one number, and a cosine that rounding pushed a hair above 1 becomes nan the moment an inverse cosine (the function that turns a cosine back into its angle) receives it. Section 2.2 built the 1-D array and its element-wise arithmetic, and this lesson reads the same array as a point with a length and a direction.


Goal

  • Build a listing's feature vector from a record in one fixed feature order, and keep that order across every vector in a calculation.
  • Compute a dot product with np.dot, @ and (a * b).sum(), and read the error two vectors of different lengths raise.
  • Predict a listing's price as w @ x + b and apply one weight vector to several listings.
  • Compute a vector's length, the cosine of the angle between two vectors and the angle in degrees, and classify a pair from the sign of its dot product.
  • Compare a float result with an exact value through np.isclose, and clip a cosine before taking its inverse.

Prerequisites

None of this is required, the lesson builds everything it uses from Section 2.2's arrays. Both are here if you want the geometry pictured before you compute it.

  • YouTube - "Vectors | Chapter 1, Essence of linear algebra" by 3Blue1Brown. A vector as an arrow and as a list of numbers at once, with adding and scaling drawn on a grid.
  • Official docs - NumPy, numpy.dot. The reference entry, whose first case is this lesson's: two 1-D arrays give their inner product, another name for the dot product.

Table of Contents

  • Vectors as Arrays
  • The Dot Product
  • The Dot Product as a Prediction
  • Length, Angle and Orthogonality

Theory

Nothing to install, every block runs on the numpy that Lesson 2.2.1 installed into (bootcamp).

Vectors as Arrays

What. A vector is an ordered list of numbers, and in NumPy it is a 1-D array. A listing's features in a fixed order, area_sqm, rooms and age, make one vector with one entry per feature, and geometrically the listing is a point in a space with one axis per feature. Why. Every calculation in this section sees a listing only through its vector, and an array carries no feature names, so the position of each number is the only thing that says what it means. When. Every time a record's numeric features enter a calculation: a prediction, a comparison between two listings, a change applied to one.

A listing of 80 square meters with 3 rooms, built 20 years ago, is np.array([80, 3, 20]), a vector with three entries. With two features you could draw it as a point on a grid, area along one axis and rooms along the other, and a third feature adds a third axis. A table of dozens of features has dozens of axes, which nobody draws, and every operation in this section works the same way at any count.

import numpy as np

x = np.array([80, 3, 20])  # area_sqm, rooms, age

print(x.shape)             # (3,)
print(x[1])                # 3

Adding two vectors and scaling one by a number are Lesson 2.2.3's element-wise arithmetic, read geometrically. Adding moves the point. Scaling slides it along the line through the origin, the all-zero point: outward for a factor above 1, inward for one between 0 and 1, and through to the opposite side for a negative one.

import numpy as np

x = np.array([80, 3, 20])         # area_sqm, rooms, age
extension = np.array([15, 1, 0])  # 15 sqm and one more room

print(x + extension)              # [95  4 20]
print(2 * x)                      # [160   6  40]

The order of the features is a contract, and every vector in one calculation must keep it. Listings rarely arrive as arrays. They arrive as records, and a dictionary's values come out in the order its keys were inserted (1.4.3), which is whatever order the source happened to write. Building the vector from one list of feature names fixes the order once, for every record.

import numpy as np

# the contract, written down once
FEATURES = ["area_sqm", "rooms", "age"]
# a record, keys in the order its source wrote them
listing = {"rooms": 3, "age": 20, "area_sqm": 80}

x = np.array([listing[name] for name in FEATURES])
x_raw = np.array(list(listing.values()))
extension = np.array([15, 1, 0])

print(x)                  # [80  3 20]
print(x_raw)              # [ 3 20 80]
print(x + extension)      # [95  4 20]
print(x_raw + extension)  # [18 21 80]

x_raw + extension describes a listing of 18 square meters with 21 rooms, built 80 years ago, and NumPy produced it without complaint, because nothing in an array says what its positions mean.

DON'T:

  • DON'T build a vector with list(record.values()), the values follow the record's own key order, not your feature list.
  • DON'T patch a vector built in the wrong order by editing its numbers, rebuild it from its record through FEATURES, so one list of names decides every position.

The Dot Product

What. The dot product of two vectors of the same length multiplies matching entries and adds the products, a . b = a[0] * b[0] + a[1] * b[1] + ..., and returns one number. NumPy computes it as np.dot(a, b), as a @ b and, spelled out, as (a * b).sum(). Why. It folds many features into one number, a price, a score, a similarity, and the next lesson's matrix product is built from nothing else. When. Whenever each entry of one vector should weigh the matching entry of another and only the total matters.

@ is Python's matrix-multiplication operator, an operator the language reserves for libraries such as NumPy to define, and between two 1-D arrays NumPy defines it as the dot product. The block pairs the listing with a second vector w, one number per feature in the same order, which the next subtitle reads as a price model's weights.

import numpy as np

x = np.array([80, 3, 20])           # area_sqm, rooms, age
w = np.array([2500, 10000, -1500])  # one number per feature, same order

print(w * x)                        # [200000  30000 -30000]
print((w * x).sum())                # 200000
print(np.dot(w, x))                 # 200000
print(w @ x)                        # 200000
print(x @ w)                        # 200000
print(type(w @ x))                  # <class 'numpy.int64'>

Written out, 2500 * 80 + 10000 * 3 + (-1500) * 20 is 200000 + 30000 - 30000. Swapping the two vectors changes nothing, because each product pairs the same two entries either way. The result is a single NumPy number, an np.int64 here and an np.float64 when either vector holds float64 values, which print shows as a plain number and the interactive prompt echoes as np.int64(200000).

The two vectors must have the same length. A weight vector that leaves a feature out raises a ValueError before anything is multiplied.

# short_weights.py
import numpy as np

x = np.array([80, 3, 20])          # area_sqm, rooms, age
w_short = np.array([2500, 10000])  # the age weight left out
print(w_short @ x)
Traceback (most recent call last):
  File "/home/goku/dev/bootcamp/short_weights.py", line 6, in <module>
    print(w_short @ x)
          ~~~~~~~~^~~
ValueError: matmul: Input operand 1 has a mismatch in its core dimension 0, with gufunc signature (n?,k),(k,m?)->(n?,m?) (size 3 is different from 2)

Operand 1 is the right-hand vector, counting from 0, and (size 3 is different from 2) sets its length against the 2 the left-hand vector brought. The signature in the middle is the shape rule behind every @, which Lesson 2.3.2 reads in full. np.dot(w_short, x) raises the same ValueError in other words, shapes (2,) and (3,) not aligned: 2 (dim 0) != 3 (dim 0).

DON'T:

  • DON'T write * where one number is wanted, w * x stops at the three products, [200000 30000 -30000], and the adding up is still to do.
  • DON'T expect a shorter vector to be padded or trimmed, a weight vector one feature short raises ValueError naming both lengths, and that loud failure is the better kind, because the order mistake of the previous subtitle raises nothing.

The Dot Product as a Prediction

What. A linear model predicts a listing's price as the dot product of a weight vector with the listing's feature vector, plus one number called the bias: price = w . x + b, in NumPy w @ x + b. Why. Each weight is the change in price per unit of its feature with the other features held fixed, so the model reads entry by entry, and the same few numbers price every listing. When. Every time a linear model scores a listing. The weights here are given, and Module 3 fits them from data, in 3.1.

import numpy as np

x = np.array([80, 3, 20])           # area_sqm, rooms, age
w = np.array([2500, 10000, -1500])  # price per unit of each feature
b = 50000                           # the bias

print(w @ x + b)                    # 250000

The weights say 2500 per square meter, 10000 per room and minus 1500 per year of age. The bias is the price the model gives a listing whose features are all zero, a fixed amount inside every prediction. Moving one feature by one unit moves the price by that feature's weight.

import numpy as np

x = np.array([80, 3, 20])
w = np.array([2500, 10000, -1500])
b = 50000
base = w @ x + b

one_more_sqm = x + np.array([1, 0, 0])
ten_years_older = x + np.array([0, 0, 10])
print(w @ one_more_sqm + b - base)     # 2500
print(w @ ten_years_older + b - base)  # -15000

The weights are the model. Learned once, they price every listing with the same line, as the loop below shows, and Lesson 2.3.2 replaces that loop with a single matrix product.

import numpy as np

w = np.array([2500, 10000, -1500])  # area_sqm, rooms, age
b = 50000
listings = [
    np.array([80, 3, 20]),
    np.array([120, 4, 5]),
    np.array([55, 2, 40]),
]
for x in listings:
    print(w @ x + b)
250000
382500
147500

The feature-order contract matters most here. Weights stored in a different order from the features still multiply and add, and the price comes out confidently wrong.

import numpy as np

x = np.array([80, 3, 20])                   # area_sqm, rooms, age
w_swapped = np.array([10000, 2500, -1500])  # rooms, area_sqm, age
b = 50000

print(w_swapped @ x + b)                    # 827500

The 80 square meters were priced as 80 rooms at 10000 each. Every multiplication was legal, so nothing raised, and the defense is building every vector, the weights included, from the same list of feature names.

DON'T:

  • DON'T store the weights in a different order from the features, the dot product pairs entries by position and never checks what a position means.
  • DON'T drop the bias, w @ x alone is 200000, the prediction minus b.

Length, Angle and Orthogonality

What. A vector's length is the square root of its dot product with itself, |v| = sqrt(v . v), in NumPy np.sqrt(v @ v), which Lesson 2.3.3 names the norm. The angle theta between two vectors comes from cos(theta) = u . v / (|u| |v|), and two vectors whose dot product is zero are orthogonal, the word for perpendicular. Why. The sign of the dot product says whether two listings point the same way, and the cosine compares their directions with both lengths divided out, which is how similarity between listings is scored. When. When two profiles are compared regardless of their size, when two directions are checked for having nothing in common, and whenever a computed float should land on an exact value.

These examples use standardized features, Lesson 2.2.5's mean 0 and standard deviation 1 per feature, so each entry says how many standard deviations a listing sits above the average (positive) or below it (negative), and a vector can point in any direction.

import numpy as np

u = np.array([2, 1, 2])  # standardized area_sqm, rooms, age

print(np.sqrt(u @ u))    # 3.0

Dividing the dot product by both lengths leaves the cosine of the angle between the vectors. np.arccos, the inverse cosine, turns that cosine into its angle in radians (the unit in which a full turn is 2 * pi, about 6.28), and np.degrees converts radians to degrees.

import numpy as np

u = np.array([2, 1, 2])
v = np.array([2, 2, 1])

cos = (u @ v) / (np.sqrt(u @ u) * np.sqrt(v @ v))
print(cos)                         # 0.8888888888888888
print(np.arccos(cos))              # 0.47588224966041665
print(np.degrees(np.arccos(cos)))  # 27.266044450732828

The sign of the dot product sorts every pair into three cases, and the same listing u against three others shows all of them.

import numpy as np

u = np.array([2, 1, 2])      # above average on every feature
others = [
    np.array([2, 2, 1]),     # also above average on every feature
    np.array([-1, 2, 0]),    # less area, more rooms, average age
    np.array([-2, -2, -1]),  # below average on every feature
]
for v in others:
    cos = (u @ v) / (np.sqrt(u @ u) * np.sqrt(v @ v))
    print(u @ v, np.round(np.degrees(np.arccos(cos)), 1))
8 27.3
0 90.0
-8 152.7

Each product in a dot product is positive where the two entries sit on the same side of average and negative where they sit on opposite sides. A positive total means the agreements outweigh the disagreements and the angle is under 90 degrees. Zero means they cancel exactly and the vectors are orthogonal, at 90 degrees unless one of them is all zeros, which has no direction and so no angle. A negative total means the disagreements win and the angle is over 90 degrees. The size of the dot product grows with both lengths as well, so only its sign speaks about direction alone.

Used as a score, the cosine is called cosine similarity: 1 for the same direction, 0 for perpendicular, -1 for opposite directions. With both lengths divided out it compares direction alone, so a listing and one exactly twice its size in every feature should score 1.

import numpy as np

x = np.array([80, 3, 20])    # area_sqm, rooms, age
y = 2 * x                    # twice the area, rooms and age

cos = (x @ y) / (np.sqrt(x @ x) * np.sqrt(y @ y))
print(cos)                   # 1.0000000000000002
print(cos == 1.0)            # False
print(np.isclose(cos, 1.0))  # True

Both square roots are stored rounded, Lesson 1.1.2's floats that cannot hold most values exactly, so the cosine lands one rounding step above 1 and == reports False for two vectors that point exactly the same way. np.isclose(a, b) returns True when the two differ by no more than a small tolerance, the comparison Module 1 asked for in place of == on floats. On float features the size of these, the orthogonality test is np.isclose(u @ v, 0.0) for the same reason, and Lesson 2.3.4 shows why much larger values need a test that scales with them.

An inverse cosine is defined only from -1 to 1, so that one rounding step is enough to break the angle.

# arccos_clip.py
import numpy as np

x = np.array([80, 3, 20])
y = 2 * x
cos = (x @ y) / (np.sqrt(x @ x) * np.sqrt(y @ y))

print(np.arccos(cos))
print(np.arccos(np.clip(cos, -1.0, 1.0)))
/home/goku/dev/bootcamp/arccos_clip.py:8: RuntimeWarning: invalid value encountered in arccos
  print(np.arccos(cos))
nan
0.0

np.clip (2.2.3) pulls the cosine back inside the range the inverse cosine accepts, and the angle comes out as 0.0, the right answer for two vectors pointing the same way.

DON'T:

  • DON'T compare a float result with an exact value using ==, use np.isclose, and np.isclose(u @ v, 0.0) to test float features the size of these for orthogonality.
  • DON'T hand a computed cosine straight to np.arccos, clip it with np.clip(cos, -1.0, 1.0) first.
  • DON'T call len(v) a vector's length in this sense, len(np.array([3, 4])) is 2, the number of entries, while the same vector's np.sqrt(v @ v) is 5.0.
  • DON'T take the cosine of an all-zero vector, such as a standardized listing sitting exactly at the average, it has no direction, and the division returns nan with RuntimeWarning: invalid value encountered in scalar divide.

Lab Exercises

Lab Exercise 1: Three Records in One Order, and One Number From Two Vectors

Three listing records arrive as dictionaries with their keys in different orders.

records = [
    {"rooms": 4, "area_sqm": 120, "age": 5},
    {"age": 40, "rooms": 2, "area_sqm": 55},
    {"area_sqm": 95, "age": 12, "rooms": 3},
]

Build each record into a vector in the order FEATURES = ["area_sqm", "rooms", "age"] and print the three vectors. Then print what np.array(list(records[1].values())) builds, with a comment saying which feature landed in which slot.

With the weights w = np.array([2500, 10000, -1500]), compute the dot product of w with the first listing's vector three ways and print all three. Print w * x for the same listing, with a comment saying what it is instead. Finally, compute w[:2] @ x inside a try, catch the ValueError and print its message.

Lab Exercise 2: Price Three Listings, Then Compare Profiles by Angle

Using the three vectors from Lab Exercise 1, the same w and the bias b = 50000, print each listing's predicted price from one loop. Then show with two predictions that one more room changes the third listing's price by the rooms weight.

Next, take the standardized profile p = np.array([1, 2, 2]) and compare it with [2, 2, 0], [2, 1, -2] and [0, -1, -1]. For each, print the dot product, the cosine rounded to 3 places, the angle in degrees rounded to 1 place, and a label, same way, perpendicular or apart, chosen from the sign of the dot product.

Finally, compute the cosine between the first listing's vector and three times that vector, and print it, its comparison with 1.0 by == and its comparison by np.isclose.


Solution 1: Three Records in One Order, and One Number From Two Vectors
import numpy as np

FEATURES = ["area_sqm", "rooms", "age"]
records = [
    {"rooms": 4, "area_sqm": 120, "age": 5},
    {"age": 40, "rooms": 2, "area_sqm": 55},
    {"area_sqm": 95, "age": 12, "rooms": 3},
]

# one comprehension per record, names in FEATURES order
vectors = [np.array([r[n] for n in FEATURES]) for r in records]
for v in vectors:
    print(v)

# the age, 40, lands in the area slot and the area, 55, in the age slot
print(np.array(list(records[1].values())))
[120   4   5]
[55  2 40]
[95  3 12]
[40  2 55]
import numpy as np

w = np.array([2500, 10000, -1500])  # area_sqm, rooms, age
x = np.array([120, 4, 5])           # the first listing

print(np.dot(w, x))                 # 332500
print(w @ x)                        # 332500
print((w * x).sum())                # 332500
# the three terms of the dot product, not yet added
print(w * x)                        # [300000  40000  -7500]

try:
    print(w[:2] @ x)                # two weights against three features
except ValueError as err:
    print(err)
matmul: Input operand 1 has a mismatch in its core dimension 0, with gufunc signature (n?,k),(k,m?)->(n?,m?) (size 3 is different from 2)

DON'T:

  • DON'T catch the ValueError and carry on with a default, a weight vector of the wrong length means the model and the data disagree about the features, and the run should stop.
  • DON'T read w * x as the dot product, it is the dot product's three terms with the adding left undone.
Solution 2: Price Three Listings, Then Compare Profiles by Angle
import numpy as np

w = np.array([2500, 10000, -1500])  # area_sqm, rooms, age
b = 50000
vectors = [np.array([120, 4, 5]), np.array([55, 2, 40]), np.array([95, 3, 12])]

for x in vectors:
    print(w @ x + b)

x = vectors[2]
one_more_room = x + np.array([0, 1, 0])
print(w @ one_more_room + b - (w @ x + b))
382500
147500
299500
10000
import numpy as np

p = np.array([1, 2, 2])
for q in (np.array([2, 2, 0]), np.array([2, 1, -2]), np.array([0, -1, -1])):
    dot = p @ q
    cos = dot / (np.sqrt(p @ p) * np.sqrt(q @ q))
    angle = np.degrees(np.arccos(np.clip(cos, -1.0, 1.0)))
    if dot > 0:
        label = "same way"
    elif dot == 0:  # integers, so == is exact here
        label = "perpendicular"
    else:
        label = "apart"
    print(dot, np.round(cos, 3), np.round(angle, 1), label)
6 0.707 45.0 same way
0 0.0 90.0 perpendicular
-4 -0.943 160.5 apart
import numpy as np

x = np.array([120, 4, 5])
y = 3 * x

cos = (x @ y) / (np.sqrt(x @ x) * np.sqrt(y @ y))
print(cos)                   # 1.0000000000000002
print(cos == 1.0)            # False
print(np.isclose(cos, 1.0))  # True

DON'T:

  • DON'T label a pair with == 0 on a float dot product, it is exact here only because the profiles are whole numbers, and on float features of this size the test is np.isclose(dot, 0.0).
  • DON'T drop the clip because the cosines in front of you happen to be safe, the first listing against its triple produced 1.0000000000000002, which np.arccos turns into nan.

Theory Assessment

Q1: The Products, the Sum and the Missing Weight

For w = [2500, 10000, -1500] and x = [80, 3, 20], explain step by step what w @ x computes, and why w * x is not the same kind of result. Then say what happens when w has two entries and x three, and why that outcome is better than a weight vector that has all three entries but in the wrong order.

Hint: Count how many numbers each expression returns. For the second half, ask which of the two mistakes produces a number at all.

Q2: What a Weight Says

A price model has w = [2500, 10000, -1500] for [area_sqm, rooms, age] and b = 50000. Say what each weight and the bias mean, by how much the predicted price moves when a listing ages 10 years, and why the same w and b serve a 55 square meter listing and a 120 square meter one. Then a teammate builds listing vectors as [rooms, area_sqm, age] and leaves w as it is. What does the model predict for the listing with 80 square meters, 3 rooms and age 20?

Hint: A weight is a rate. For the teammate's vector, write out the three products the dot product forms.

Q3: Signs, Angles and a Pair That Should Score 1

Three pairs of standardized listings have dot products 8, 0 and -8. Say what each sign tells you about the angle between the pair and about how their features compare. Then explain why a listing and its exact double should have cosine similarity 1, why the lesson's run printed 1.0000000000000002, and what that value does to np.arccos.

Hint: A dot product is a sum of one product per feature, and a product is positive when both entries sit on the same side of average. For the second half, ask which steps of the cosine produce a number a float cannot hold exactly.

Q4: The Most Similar Listing

A teammate ranks candidate listings by their dot product with a query listing and calls the top one the most similar. Read the following without running it, then state what each line prints.

import numpy as np

query = np.array([2, 1, 2])  # standardized area_sqm, rooms, age
candidates = [
    np.array([2, 1, 2]),
    np.array([4, 4, 1]),
    np.array([1, 0, 1]),
]
for c in candidates:
    cos = (query @ c) / (np.sqrt(query @ query) * np.sqrt(c @ c))
    print(query @ c, np.round(cos, 3), cos == 1.0)

Which candidate does the teammate's rule pick, which does cosine similarity pick, and why do the two rankings disagree? Then explain why the first line's last value is True, when the lesson's cosine of a listing and its double compared False.

Hint: The dot product grows with both lengths. For the last part, work out every number the first candidate's cosine is built from, and ask which of them a float stores exactly.


Answer 1:

w @ x multiplies matching entries, 2500 * 80 = 200000, 10000 * 3 = 30000 and -1500 * 20 = -30000, then adds the three products, 200000. It returns one number. w * x stops after the multiplying and returns the three products as an array, [200000 30000 -30000], the dot product's terms rather than its value, and (w * x).sum() is the missing step.

With two weights and three features, w @ x raises ValueError before anything is multiplied, and the message names the two lengths, (size 3 is different from 2). That is the better outcome, because it stops the script on the line with the bug. Weights in the wrong order have the right length, every multiplication is legal, and the result is a number that is simply wrong, 827500 for a listing the model prices at 250000.

DON'T treat a result's shape as a detail, one number is a dot product and three numbers are not.

Answer 2:

Each weight is a rate, the change in predicted price for one unit of its feature with the other features held fixed: 2500 per square meter, 10000 per room and minus 1500 per year of age. The bias is what the model predicts for a listing whose features are all zero, a fixed amount inside every prediction. Ten more years move the price by 10 * -1500, which is -15000. One w and one b price every listing because those four numbers are the whole model, and only x changes from listing to listing.

The teammate's vector for that listing is [3, 80, 20], so the dot product forms 2500 * 3 + 10000 * 80 - 1500 * 20, and with the bias the prediction is 827500. That is exactly what the swapped weights produced, because swapping the same two positions in either vector pairs the same numbers. Nothing raises in either case.

DON'T fix an order bug by reordering one side without checking the other, the pairing of positions is the only thing the dot product sees.

Answer 3:

A dot product of 8 is positive, so the agreements in sign outweigh the disagreements and the angle is under 90 degrees, 27.3 in the lesson's run. A dot product of 0 means they cancel exactly, the angle is 90 degrees and the pair is orthogonal. A dot product of -8 is negative, the disagreements win and the angle is over 90 degrees, 152.7 in the run.

A listing and its double point the same way, because doubling stretches a vector along its own direction, so the cosine is 1 in exact arithmetic. The run computes two square roots that a float cannot store exactly, then multiplies and divides rounded values, and the result lands one rounding step above 1, 1.0000000000000002. np.arccos is defined only from -1 to 1, so that value returns nan with a RuntimeWarning, and np.clip(cos, -1.0, 1.0) restores the angle 0.0.

DON'T read a nan from np.arccos as a fault in the vectors before checking whether the cosine strayed past 1.

Answer 4:
9 1.0 True
14 0.812 False
4 0.943 False

The teammate's rule picks the second candidate, whose dot product, 14, is the largest. Cosine similarity picks the first, identical to the query with a cosine of 1.0, and ranks the third above the second although the third has the smallest dot product. The rankings disagree because a dot product mixes length with direction: the second candidate sits far from average, its length is np.sqrt(33), about 5.74, and that length inflates its dot product while its direction is the furthest of the three from the query's. The third is short and points close to the query. Which listing has the most similar profile is a question about direction, so the cosine answers it and the raw dot product does not.

The first line compares True because nothing in that cosine rounds. The dot product is 9, both squared lengths are 9, np.sqrt(9) is exactly 3.0, and 9 / (3.0 * 3.0) is exactly 1.0, every value one a float holds exactly. The lesson's pair took square roots of numbers that are not perfect squares, and those were stored rounded. == happened to work here, code that relies on it can fail on the next pair, as the listing and its double did, so np.isclose stays the comparison.

DON'T rank similarity by raw dot product, a long vector outranks a well-aligned one, and divide by both lengths first.


Assignment

Assignment: What Moves the Price, and What Leaves It Alone

A pricing analyst is reviewing the model with weights [2500, 10000, -1500] for [area_sqm, rooms, age] and bias 50000, on a listing that arrived as the record {"age": 20, "area_sqm": 80, "rooms": 3}. Build the following in one script, building the listing's vector from the record in FEATURES order.

First, a what-if table: the change in this listing's predicted price when one feature alone moves by 1, 5 or 10 units, as a 3 by 3 array with one row per step size and one column per feature. A teammate's version, np.array([1, 5, 10]) * w, runs without an error. Print what it returns and explain in a comment what each of its three numbers is and why nothing warned you. Then build the table with np.outer, which takes two 1-D arrays a and b and returns the 2-D array whose row i, column j holds a[i] * b[j], so np.outer(a, b) has the shape (len(a), len(b)). Check the cell for five more rooms against two full predictions.

Second, find a renovation the model cannot see: a change vector of whole numbers that alters at least two features of the listing yet leaves its predicted price exactly where it was. Prove it twice, with the dot product of the change and the weights, and with two full predictions. Print the angle between the change vector and the weight vector, find a second such change that alters a different pair of features, and say in a comment what every change the model ignores has in common.

Hints:

  • np.outer(a, b)[i, j] is a[i] * b[j], so the array whose entries should run down the rows goes first.
  • Adding a change delta to x moves the prediction by w @ delta, because the bias cancels and w @ (x + delta) is w @ x + w @ delta. Look for two features whose weights trade off exactly.

Solution:
import numpy as np

FEATURES = ["area_sqm", "rooms", "age"]
record = {"age": 20, "area_sqm": 80, "rooms": 3}
x = np.array([record[name] for name in FEATURES])
w = np.array([2500, 10000, -1500])
b = 50000
base = w @ x + b
steps = np.array([1, 5, 10])

print(x)                               # [80  3 20]
print(base)                            # 250000
print(steps * w)                       # [  2500  50000 -15000]
# Step 1 paired with area, step 5 with rooms and step 10 with age, three
# of the table's nine cells, one from each row. Nothing warned, because
# 'steps' and 'w' have the same length, 3, and '*' pairs by position.

table = np.outer(steps, w)
print(table)

five_more_rooms = x + np.array([0, 5, 0])
print(w @ five_more_rooms + b - base)  # 50000
print(table[1, 1])                     # 50000
[[  2500  10000  -1500]
 [ 12500  50000  -7500]
 [ 25000 100000 -15000]]
# continuing the script above
delta = np.array([4, -1, 0])         # 4 sqm more, one room fewer
print(w @ delta)                     # 0
print(w @ (x + delta) + b - base)    # 0

cos = (w @ delta) / (np.sqrt(w @ w) * np.sqrt(delta @ delta))
print(np.degrees(np.arccos(cos)))    # 90.0

delta_2 = np.array([3, 0, 5])        # 3 sqm more, 5 years older
print(w @ delta_2)                   # 0
print(w @ (x + delta_2) + b - base)  # 0
# Every change the model ignores is orthogonal to the weight vector.
# The price moves by w @ delta, so a change at 90 degrees to w is
# invisible to the model, whatever it does to the listing.

The teammate's three numbers are the diagonal of the table, the cells where the row's step and the column's feature happen to share a position. With four step sizes the same line raises the broadcasting ValueError, and with exactly three it returned a plausible array, which is the lesson's order warning in a new place.

np.outer multiplies every entry of steps by every entry of w, so row i is steps[i] * w, the whole weight vector scaled by one step size. The dot product and the outer product are opposites in what they keep: the dot product multiplies only matching entries and adds everything into one number, while the outer product multiplies every pair and adds nothing.

The renovations that leave the price alone are the deeper result. A change of 4 square meters is worth 4 * 2500, exactly one room at 10000, so gaining the area and losing the room cancel. Three more square meters, 7500, cancel five more years of age, 5 * -1500. Both changes have a dot product of zero with w, so both sit at 90 degrees to it, and the price, which moves by w @ delta, cannot see either. Every listing the model prices at 250000 is x plus some change orthogonal to w.

DON'T let two same-length arrays meet in * when you meant every pair, element-wise pairing gives no error whenever the lengths happen to match, and don't read an unchanged prediction as an unchanged listing, the change may simply be orthogonal to the weights.


Additional Resources

Optional, and nothing later depends on either. Worth opening if you want the geometry behind the formula or the exact rule behind np.isclose.

  • YouTube - "Dot products and duality | Chapter 9, Essence of linear algebra" by 3Blue1Brown. Why multiplying matching entries and adding them up agrees with the geometric picture of one vector projected onto another.
  • Official docs - NumPy, numpy.isclose. The inequality np.isclose tests, its default tolerances rtol=1e-05 and atol=1e-08, and the warning that the absolute one is too loose for numbers much smaller than one.

Summary

The array from Section 2.2 did not change in this lesson, only the way it is read. A row of three numbers stopped being a container and became a point with a position, a length and a direction, and that one shift turns a handful of element-wise habits into geometry. Adding a vector moves a listing, scaling one stretches it, and the question of how alike two listings are becomes a question about an angle, which a few lines of NumPy answer.

The dot product is the hinge. Multiplying matching entries and adding them up is about the plainest arithmetic there is, and it turns out to be both the way a linear model prices a listing and the way two profiles are compared. The weights of a price model are not mysterious: each is a rate, read entry by entry, and the prediction is a sum of rates times amounts plus the bias. The same sum, divided by two lengths, measures direction, and its sign alone sorts every pair into agreeing, unrelated and opposed.

What the operation does not do matters as much as what it does. It checks that the two lengths agree and trusts you for everything else, so the meaning of every position lives outside the array, in a list of feature names that is written down once and obeyed everywhere. Its answers are exact in arithmetic and rounded in memory, so a result that should land on 1 or on 0 is judged with a tolerance rather than with equality. The next lesson stacks many vectors into one table and runs every dot product at once, and both disciplines come along unchanged.


Cheat Sheet

Part A - Reference Table

Concept Formula NumPy Note
Vector features in a fixed order np.array([80, 3, 20]) the order is a contract
Vector from a record one value per feature name np.array([rec[n] for n in FEATURES]) never list(rec.values())
Dot product a . b, the sum of a[i] * b[i] a @ b, np.dot(a, b), (a * b).sum() one number, lengths must match
Element-wise product one product per entry a * b not a dot product
Prediction w . x + b w @ x + b a weight is price per unit
Length sqrt(v . v) np.sqrt(v @ v) the norm, Lesson 2.3.3
Cosine dot product over both lengths (u @ v) / (np.sqrt(u @ u) * np.sqrt(v @ v)) 1 same way, 0 perpendicular, -1 opposite
Angle arccos(cos) np.degrees(np.arccos(np.clip(cos, -1.0, 1.0))) clip first
Orthogonal u . v = 0 np.isclose(u @ v, 0.0) float features of moderate size, never ==
Outer product a[i] * b[j] for every pair np.outer(a, b) shape (len(a), len(b))

Part B - Compact Code Block

import numpy as np

FEATURES = ["area_sqm", "rooms", "age"]
x = np.array([rec[n] for n in FEATURES])  # fixed feature order
w = np.array([2500, 10000, -1500])        # price per unit
price = w @ x + 50000                     # dot product plus bias
w * x                                     # three products, not one

u, v = np.array([2, 1, 2]), np.array([2, 2, 1])
np.sqrt(u @ u)                            # length, 3.0
cos = (u @ v) / (np.sqrt(u @ u) * np.sqrt(v @ v))
angle = np.degrees(np.arccos(np.clip(cos, -1.0, 1.0)))
np.isclose(cos, 1.0)                      # never == on floats
np.outer([1, 5, 10], w)                   # every pair multiplied
←Previous: Section 2, Lesson 5
Next: Lesson 2→

Questions about this lesson: #module-2 on Discord

Found an error in this lesson? Report it

Your progress
Current streak
🔥 0
Lessons marked today
0 / 3
Longest streak
0

Sign in

The bootcamp

  • Curriculum
  • How To
  • Pricing
  • Changelog
  • Future

The community

  • Discord
  • Reviews
  • Contact

Follow

  • Instagram
  • X
  • TikTok
  • Reddit

Legal

  • Terms
  • Privacy
  • Refunds

Coming soon on November 16, 2026

Theme