Several predictors at once, "holding the others constant" — the matrix form b = (XᵀX)⁻¹Xᵀy, built from scratch, with standard errors, R², adjusted R², and the F-test.
Real outcomes have more than one cause. A house's price depends on its size, its age, its neighbourhood; a salary on experience, education, and role. Multiple regression estimates each predictor's effect while holding the others fixed, which is the closest thing to a controlled experiment you can get from observational data. It's the model behind most economics, epidemiology, and pricing work, and the foundation of the machine-learning models that came after it.
The model. ŷ = b₀ + b₁x₁ + b₂x₂ + … + b_k·x_k. Each coefficient b_j is the change in the prediction when x_j rises by one unit and every other predictor stays the same. That "holding constant" is the whole point — and it's why coefficients change when you add or remove predictors.
The matrix form. Stack the data into a design matrix X — one row per observation, a first column of 1s for the intercept, one column per predictor. Least squares then has a one-line answer, the normal equations: (XᵀX)b = Xᵀy, so b = (XᵀX)⁻¹Xᵀy. XᵀX is small (one row and column per parameter) and symmetric positive definite, so the Cholesky factorization XᵀX = L·Lᵀ solves it cleanly.
Standard errors. The variance of each coefficient is σ² times the matching diagonal entry of (XᵀX)⁻¹, where σ is estimated by the residual standard error √(SSE/(n − p)) and p counts all parameters including the intercept. Each coefficient's t-test asks "does this predictor help, given all the others?"
R² always rises; adjusted R² doesn't. Adding any predictor — even random noise — can only lower SSE, so R² never falls. Adjusted R² = 1 − (1 − R²)(n − 1)/(n − p) charges for each parameter and drops when a predictor doesn't pull its weight.
The F-test asks whether the predictors, together, explain anything at all: it compares the variation the model explains per predictor with the leftover variation per residual degree of freedom.
Multicollinearity. When predictors move together (size and bedroom count), the data can't separate their effects well. The overall fit stays good, but the individual coefficients get large standard errors and can swing wildly or flip sign. If one predictor is an exact combination of others, XᵀX has no inverse at all.
Categorical predictors enter as dummy variables: one 0/1 column per category except a baseline. Each dummy's coefficient is that category's difference from the baseline, holding the other predictors fixed.
| Quantity | Formula | In words |
|---|---|---|
| Coefficients | b = (XᵀX)⁻¹Xᵀy | the normal equations |
| Residual SE | σ̂ = √(SSE / (n − p)) | p includes the intercept |
| Coefficient SE | σ̂·√((XᵀX)⁻¹ⱼⱼ) | |
| Coefficient test | t = bⱼ / SE(bⱼ), df = n − p | useful given the others? |
| R² | 1 − SSE/SST | never falls when you add predictors |
| Adjusted R² | 1 − (1 − R²)(n − 1)/(n − p) | can fall |
| F statistic | ((SST − SSE)/(p − 1)) / (SSE/(n − p)) | any predictor useful? |
| Cholesky | a = L·Lᵀ, L lower triangular | how to invert XᵀX |
type Matrix [][]float64 with Rows() and Cols() reads naturally, and a Matrix still indexes like any [][]float64.make(Matrix, rows) creates rows that are all nil; each needs its own make([]float64, cols). NewMatrix does it once, correctly.Mul, the i-k-j order (for k, aik := range a[i] then for j, bkj := range b[k]) walks memory row by row; the textbook i-j-k order jumps between rows of b on every step.Cholesky notices when a pivot isn't positive and returns ErrNotPositiveDefinite; callers check it with errors.Is.DesignMatrix(cols ...[]float64) — take any number of predictor columns; pass a slice of them with DesignMatrix(cols...).package main import "fmt" // centred returns Σ(a − ā)(b − b̄). func centred(a, b []float64) float64 { n := float64(len(a)) var ma, mb float64 for i := range a { ma += a[i] mb += b[i] } ma, mb = ma/n, mb/n s := 0.0 for i := range a { s += (a[i] - ma) * (b[i] - mb) } return s } func main() { // 15 made-up house sales: bedrooms, size (hundreds of sq ft), price ($k). beds := []float64{3, 3, 2, 4, 4, 3, 3, 2, 5, 3, 2, 4, 3, 4, 3} size := []float64{14.2, 16.8, 12.5, 19.6, 22.1, 15.4, 18.3, 11.9, 24.6, 17.2, 13.8, 20.5, 16.1, 21.7, 14.9} price := []float64{220.4, 252.2, 172.0, 304.6, 352.2, 215.8, 277.6, 149.8, 389.6, 246.4, 192.6, 323.0, 236.8, 329.2, 224.2} // price on bedrooms alone: slope = S(beds, price) / S(beds, beds) alone := centred(beds, price) / centred(beds, beds) // price on bedrooms AND size: the two-predictor least-squares formulas s11, s22, s12 := centred(beds, beds), centred(size, size), centred(beds, size) s1y, s2y := centred(beds, price), centred(size, price) det := s11*s22 - s12*s12 bBeds := (s1y*s22 - s2y*s12) / det bSize := (s2y*s11 - s1y*s12) / det fmt.Printf("price ~ bedrooms: %+6.1f $k per bedroom\n", alone) fmt.Printf("price ~ bedrooms + size: %+6.1f $k per bedroom, %+5.1f $k per 100 sq ft\n", bBeds, bSize) }
It prints:
On its own, each bedroom seems to add $77k, because houses with more bedrooms are also bigger, and the simple regression credits bedrooms with the size's effect. Holding size fixed, an extra bedroom is worth about $13k. Neither number is "wrong" — they answer different questions — but only the second is the effect of a bedroom. (The two predictors correlate at 0.95, which is why the bedroom coefficient in the full model has a large standard error.)
Part A builds the matrix tools: transpose, multiply, Cholesky, triangular solves, and the inverse of a symmetric positive definite matrix. Part B fits the model and fills in every field of Model, then predicts and tests each coefficient. Part C turns a categorical variable into dummy columns.
go test ./08-multiple-regression/
The expected values come from statsmodels' OLS on 15 made-up house sales (and from NumPy for the matrix tests). The house data was generated as 50 + 12·size + 8·beds − 1.2·age + noise; look at how far the fitted coefficients land from those true values, and at which ones the t-tests can tell from zero.
Everything below is also in the course download, in 08-multiple-regression/. Work in exercises.go; the tests decide when you’re done.
// Package multireg is Module 8: multiple regression. // // Several predictors at once: ŷ = b₀ + b₁x₁ + … + b_k·x_k. Written with // matrices it's one equation — b = (XᵀX)⁻¹Xᵀy — so Part A builds the matrix // tools first, then Part B uses them to fit and test the model. package multireg import ( "errors" "github.com/bozcode/go-statistics-and-regression/internal/special" ) // Matrix is a dense matrix stored row by row: m[i][j] is row i, column j. type Matrix [][]float64 // NewMatrix returns a rows×cols matrix of zeros. func NewMatrix(rows, cols int) Matrix { m := make(Matrix, rows) for i := range m { m[i] = make([]float64, cols) } return m } // Rows returns the number of rows. func (m Matrix) Rows() int { return len(m) } // Cols returns the number of columns (0 for an empty matrix). func (m Matrix) Cols() int { if len(m) == 0 { return 0 } return len(m[0]) } // ErrNotPositiveDefinite is returned by Cholesky when the matrix isn't // symmetric positive definite. For XᵀX that means a predictor is a perfect // combination of the others (perfect multicollinearity). var ErrNotPositiveDefinite = errors.New("multireg: matrix is not positive definite") // Model is a fitted multiple regression. Index 0 of Coef and SE is the // intercept; index j is predictor j. type Model struct { Coef []float64 // b₀, b₁, …, b_k SE []float64 // standard error of each coefficient R2 float64 // 1 − SSE/SST AdjR2 float64 // R² penalized for the number of predictors Sigma float64 // residual standard error √(SSE/(n − p)) F float64 // overall F statistic: do the predictors explain anything? FP float64 // p-value of F N, P int // observations; parameters including the intercept } // The helpers below are given — the t and F distributions from // internal/special (you built the t distribution in Module 4). // tCDF returns P(T <= t) for Student's t with df degrees of freedom. func tCDF(t, df float64) float64 { return special.TCDF(t, df) } // fSF returns the upper tail P(F > f) for an F distribution with d1 and d2 // degrees of freedom — the p-value of an F statistic. func fSF(f, d1, d2 float64) float64 { return special.FSF(f, d1, d2) }
//go:build !solutions package multireg // ============================================================ // Part A — the matrix tools // ============================================================ // Transpose returns aᵀ: row i of the result is column i of a. func Transpose(a Matrix) Matrix { // TODO return nil } // Mul returns the product ab: entry (i, j) is row i of a dotted with // column j of b. a.Cols() must equal b.Rows(). Target: O(rows·cols·inner). func Mul(a, b Matrix) Matrix { // TODO return nil } // Cholesky factors a symmetric positive definite matrix as a = L·Lᵀ, with L // lower triangular. Column by column: // // L[j][j] = √(a[j][j] − Σ_{k<j} L[j][k]²) // L[i][j] = (a[i][j] − Σ_{k<j} L[i][k]·L[j][k]) / L[j][j] for i > j // // If a diagonal value under the square root is <= 0, return // ErrNotPositiveDefinite. func Cholesky(a Matrix) (Matrix, error) { // TODO return nil, nil } // SolveCholesky solves (L·Lᵀ)x = b given the Cholesky factor L: first // forward substitution for L·z = b (top row down), then back substitution // for Lᵀ·x = z (bottom row up). Target: O(n²). func SolveCholesky(l Matrix, b []float64) []float64 { // TODO return nil } // InverseSPD returns a⁻¹ for a symmetric positive definite matrix: factor // once with Cholesky, then solve for each column of the identity. func InverseSPD(a Matrix) (Matrix, error) { // TODO return nil, nil } // ============================================================ // Part B — fitting the model // ============================================================ // DesignMatrix builds X from predictor columns: a first column of 1s (for // the intercept), then one column per predictor. With n observations and k // predictors, X is n×(k+1). func DesignMatrix(cols ...[]float64) Matrix { // TODO return nil } // Fit fits y = Xb by least squares and fills in the whole Model: // // b = (XᵀX)⁻¹Xᵀy (use InverseSPD on XᵀX) // SSE = Σ(y − ŷ)², SST = Σ(y − ȳ)², p = X.Cols() // R2 = 1 − SSE/SST // AdjR2 = 1 − (1 − R2)(n − 1)/(n − p) // Sigma = √(SSE/(n − p)) // SE[j] = Sigma·√((XᵀX)⁻¹[j][j]) // F = ((SST − SSE)/(p − 1)) / (SSE/(n − p)), FP = fSF(F, p−1, n−p) // // Returns ErrNotPositiveDefinite if XᵀX can't be inverted. func Fit(x Matrix, y []float64) (Model, error) { // TODO return Model{}, nil } // Predict returns b₀ + b₁x₁ + … for one observation's predictor values // (without the leading 1). func (m Model) Predict(x []float64) float64 { // TODO return 0 } // TStats returns Coef[j] / SE[j] for every coefficient. func (m Model) TStats() []float64 { // TODO return nil } // PValues returns the two-sided p-value for each coefficient's t statistic, // with n − p degrees of freedom: "is this predictor useful, given all the // others?" func (m Model) PValues() []float64 { // TODO return nil } // ============================================================ // Part C — categorical predictors // ============================================================ // Dummies turns a categorical variable into 0/1 columns (treatment // coding). The levels are sorted; the first is the baseline and gets no // column, and every other level gets one column that is 1 where the value // equals that level. Returns the columns and their level names. // ["b" "a" "c" "a"] -> columns for "b" [1 0 0 0] and "c" [0 0 1 0]. func Dummies(values []string) (cols [][]float64, names []string) { // TODO return nil, nil }
package multireg import ( "errors" "math" "slices" "testing" ) // near reports whether a and b agree to about 9 significant digits. // Expected values come from NumPy and statsmodels' OLS. func near(a, b float64) bool { return math.Abs(a-b) <= 1e-9*math.Max(1, math.Abs(b)) } // relNear checks 9 significant digits with no absolute floor, for tiny // p-values. func relNear(a, b float64) bool { return math.Abs(a-b) <= 1e-9*math.Abs(b) } func slicesNear(t *testing.T, name string, got, want []float64) { t.Helper() if len(got) != len(want) { t.Fatalf("%s has %d values, want %d", name, len(got), len(want)) } for i := range want { if !near(got[i], want[i]) { t.Errorf("%s[%d] = %v, want %v", name, i, got[i], want[i]) } } } // 15 made-up house sales: size (hundreds of sq ft), bedrooms, age (years), // price ($k). They were generated as 50 + 12·size + 8·beds − 1.2·age + noise. var ( size = []float64{14.2, 16.8, 12.5, 19.6, 22.1, 15.4, 18.3, 11.9, 24.6, 17.2, 13.8, 20.5, 16.1, 21.7, 14.9} beds = []float64{3, 3, 2, 4, 4, 3, 3, 2, 5, 3, 2, 4, 3, 4, 3} age = []float64{25, 12, 40, 8, 5, 30, 15, 45, 3, 20, 35, 10, 22, 6, 28} price = []float64{220.4, 252.2, 172.0, 304.6, 352.2, 215.8, 277.6, 149.8, 389.6, 246.4, 192.6, 323.0, 236.8, 329.2, 224.2} neigh = []string{"east", "west", "north", "east", "west", "north", "east", "north", "west", "east", "north", "west", "east", "west", "north"} ) // spd is a small symmetric positive definite matrix. var spd = Matrix{{4, 2, 0.6}, {2, 5, 1.5}, {0.6, 1.5, 3}} func TestTransposeMul(t *testing.T) { a := Matrix{{1, 2, 3}, {4, 5, 6}} at := Transpose(a) if at.Rows() != 3 || at.Cols() != 2 || at[2][0] != 3 || at[0][1] != 4 { t.Fatalf("Transpose = %v, want [[1 4] [2 5] [3 6]]", at) } p := Mul(a, at) // 2×2 want := Matrix{{14, 32}, {32, 77}} if p.Rows() != 2 || p.Cols() != 2 { t.Fatalf("Mul(a, aᵀ) is %d×%d, want 2×2", p.Rows(), p.Cols()) } for i := range want { if !slices.Equal(p[i], want[i]) { t.Fatalf("Mul(a, aᵀ) = %v, want %v", p, want) } } } func TestCholesky(t *testing.T) { l, err := Cholesky(spd) if err != nil { t.Fatalf("Cholesky: %v", err) } want := Matrix{{2, 0, 0}, {1, 2, 0}, {0.3, 0.6, 1.5968719422671311}} if l.Rows() != 3 { t.Fatalf("Cholesky returned %d rows, want 3", l.Rows()) } for i := range want { slicesNear(t, "L row", l[i], want[i]) } // a predictor that duplicates another makes XᵀX singular if _, err := Cholesky(Matrix{{1, 2}, {2, 4}}); !errors.Is(err, ErrNotPositiveDefinite) { t.Errorf("Cholesky of a singular matrix: err = %v, want ErrNotPositiveDefinite", err) } } func TestSolveCholesky(t *testing.T) { l, err := Cholesky(spd) if err != nil { t.Fatalf("Cholesky: %v", err) } slicesNear(t, "SolveCholesky", SolveCholesky(l, []float64{1, 2, 3}), []float64{0.0625, 0.09264705882352943, 0.9411764705882353}) } func TestInverseSPD(t *testing.T) { inv, err := InverseSPD(spd) if err != nil { t.Fatalf("InverseSPD: %v", err) } want := Matrix{ {0.3125, -0.125, 0}, {-0.125, 0.2852941176470588, -0.11764705882352942}, {0, -0.11764705882352942, 0.3921568627450981}, } if inv.Rows() != 3 { t.Fatalf("InverseSPD returned %d rows, want 3", inv.Rows()) } for i := range want { slicesNear(t, "inverse row", inv[i], want[i]) } } func TestDesignMatrix(t *testing.T) { x := DesignMatrix([]float64{5, 6}, []float64{7, 8}) want := Matrix{{1, 5, 7}, {1, 6, 8}} if x.Rows() != 2 || x.Cols() != 3 || !slices.Equal(x[0], want[0]) || !slices.Equal(x[1], want[1]) { t.Errorf("DesignMatrix = %v, want %v", x, want) } } func TestFit(t *testing.T) { m, err := Fit(DesignMatrix(size, beds, age), price) if err != nil { t.Fatalf("Fit: %v", err) } slicesNear(t, "Coef", m.Coef, []float64{11.950288606041, 13.10973245262107, 11.334057593889174, -0.7900514417688478}) slicesNear(t, "SE", m.SE, []float64{33.27433301780821, 1.9653731480602177, 6.961457337870392, 0.41570223566684117}) if !near(m.R2, 0.9918230904103226) || !near(m.AdjR2, 0.9895930241585924) || !near(m.Sigma, 7.023439381709451) { t.Errorf("R2, AdjR2, Sigma = %v, %v, %v; want 0.99182, 0.98959, 7.02344", m.R2, m.AdjR2, m.Sigma) } if !near(m.F, 444.75050444838416) || !relNear(m.FP, 9.296103842548068e-12) { t.Errorf("F, FP = %v, %v; want 444.75050444838416, 9.296103842548068e-12", m.F, m.FP) } if m.N != 15 || m.P != 4 { t.Errorf("N, P = %d, %d; want 15, 4", m.N, m.P) } // age duplicated: XᵀX can't be inverted if _, err := Fit(DesignMatrix(size, age, age), price); !errors.Is(err, ErrNotPositiveDefinite) { t.Errorf("Fit with a duplicated predictor: err = %v, want ErrNotPositiveDefinite", err) } } func TestPredictAndTests(t *testing.T) { m, err := Fit(DesignMatrix(size, beds, age), price) if err != nil { t.Fatalf("Fit: %v", err) } if len(m.Coef) != 4 || len(m.SE) != 4 { t.Fatalf("Fit returned %d coefficients and %d standard errors, want 4 and 4", len(m.Coef), len(m.SE)) } if got := m.Predict([]float64{18, 3, 10}); !near(got, 274.0271311171993) { t.Errorf("Predict(18, 3, 10) = %v, want 274.0271311171993", got) } slicesNear(t, "TStats", m.TStats(), []float64{0.3591443470751249, 6.670352887216406, 1.6281156435782198, -1.9005224749429148}) ps := m.PValues() want := []float64{0.7262859679619491, 3.513440911883796e-05, 0.13177752448152952, 0.08387623321614873} if len(ps) != len(want) { t.Fatalf("PValues has %d values, want %d", len(ps), len(want)) } for i := range want { if !relNear(ps[i], want[i]) { t.Errorf("PValues[%d] = %v, want %v", i, ps[i], want[i]) } } } func TestDummies(t *testing.T) { cols, names := Dummies([]string{"b", "a", "c", "a"}) if !slices.Equal(names, []string{"b", "c"}) || len(cols) != 2 || !slices.Equal(cols[0], []float64{1, 0, 0, 0}) || !slices.Equal(cols[1], []float64{0, 0, 1, 0}) { t.Fatalf("Dummies([b a c a]) = %v, %v; want [[1 0 0 0] [0 0 1 0]], [b c]", cols, names) } // price ~ size + neighbourhood, baseline "east" (statsmodels' C(neigh)) cols, names = Dummies(neigh) if !slices.Equal(names, []string{"north", "west"}) { t.Fatalf("Dummies(neigh) names = %v, want [north west]", names) } m, err := Fit(DesignMatrix(append([][]float64{size}, cols...)...), price) if err != nil { t.Fatalf("Fit: %v", err) } slicesNear(t, "Coef", m.Coef, []float64{-39.465193581427116, 17.36681461249573, -7.580166609764497, 1.570732673267437}) slicesNear(t, "SE", m.SE, []float64{19.46582176230474, 1.1174667512592549, 6.597446475670623, 7.0600201579773225}) if !near(m.R2, 0.9878742640551367) { t.Errorf("R2 = %v, want 0.9878742640551367", m.R2) } }
Try not to open solution.go until your tests pass, or until you’ve been stuck for a solid half hour. Then compare: different-but-correct is fine; notice where the reference is simpler. go test -tags solutions ./08-multiple-regression/ runs the same tests against it.
solutions · 199 lines›//go:build solutions package multireg import ( "math" "slices" ) // ============================================================ // Part A — the matrix tools // ============================================================ func Transpose(a Matrix) Matrix { t := NewMatrix(a.Cols(), a.Rows()) for i := range a { for j := range a[i] { t[j][i] = a[i][j] } } return t } func Mul(a, b Matrix) Matrix { out := NewMatrix(a.Rows(), b.Cols()) for i := range a { for k, aik := range a[i] { for j, bkj := range b[k] { out[i][j] += aik * bkj } } } return out } func Cholesky(a Matrix) (Matrix, error) { n := a.Rows() l := NewMatrix(n, n) for j := 0; j < n; j++ { d := a[j][j] for k := 0; k < j; k++ { d -= l[j][k] * l[j][k] } if d <= 1e-12*math.Abs(a[j][j]) || d <= 0 { return nil, ErrNotPositiveDefinite } l[j][j] = math.Sqrt(d) for i := j + 1; i < n; i++ { s := a[i][j] for k := 0; k < j; k++ { s -= l[i][k] * l[j][k] } l[i][j] = s / l[j][j] } } return l, nil } func SolveCholesky(l Matrix, b []float64) []float64 { n := len(b) z := make([]float64, n) for i := 0; i < n; i++ { s := b[i] for k := 0; k < i; k++ { s -= l[i][k] * z[k] } z[i] = s / l[i][i] } x := make([]float64, n) for i := n - 1; i >= 0; i-- { s := z[i] for k := i + 1; k < n; k++ { s -= l[k][i] * x[k] } x[i] = s / l[i][i] } return x } func InverseSPD(a Matrix) (Matrix, error) { l, err := Cholesky(a) if err != nil { return nil, err } n := a.Rows() inv := NewMatrix(n, n) for j := 0; j < n; j++ { e := make([]float64, n) e[j] = 1 col := SolveCholesky(l, e) for i := range col { inv[i][j] = col[i] } } return inv, nil } // ============================================================ // Part B — fitting the model // ============================================================ func DesignMatrix(cols ...[]float64) Matrix { n := len(cols[0]) x := NewMatrix(n, len(cols)+1) for i := 0; i < n; i++ { x[i][0] = 1 for j, c := range cols { x[i][j+1] = c[i] } } return x } func Fit(x Matrix, y []float64) (Model, error) { xt := Transpose(x) inv, err := InverseSPD(Mul(xt, x)) if err != nil { return Model{}, err } yCol := NewMatrix(len(y), 1) for i, v := range y { yCol[i][0] = v } bCol := Mul(inv, Mul(xt, yCol)) n, p := x.Rows(), x.Cols() m := Model{Coef: make([]float64, p), SE: make([]float64, p), N: n, P: p} for j := range m.Coef { m.Coef[j] = bCol[j][0] } ybar := 0.0 for _, v := range y { ybar += v / float64(n) } var sse, sst float64 for i := range x { fit := 0.0 for j, xij := range x[i] { fit += xij * m.Coef[j] } sse += (y[i] - fit) * (y[i] - fit) sst += (y[i] - ybar) * (y[i] - ybar) } dfRes := float64(n - p) m.R2 = 1 - sse/sst m.AdjR2 = 1 - (1-m.R2)*float64(n-1)/dfRes m.Sigma = math.Sqrt(sse / dfRes) for j := range m.SE { m.SE[j] = m.Sigma * math.Sqrt(inv[j][j]) } m.F = ((sst - sse) / float64(p-1)) / (sse / dfRes) m.FP = fSF(m.F, float64(p-1), dfRes) return m, nil } func (m Model) Predict(x []float64) float64 { y := m.Coef[0] for j, v := range x { y += m.Coef[j+1] * v } return y } func (m Model) TStats() []float64 { out := make([]float64, len(m.Coef)) for j := range out { out[j] = m.Coef[j] / m.SE[j] } return out } func (m Model) PValues() []float64 { df := float64(m.N - m.P) out := make([]float64, len(m.Coef)) for j, t := range m.TStats() { out[j] = 2 * tCDF(-math.Abs(t), df) } return out } // ============================================================ // Part C — categorical predictors // ============================================================ func Dummies(values []string) (cols [][]float64, names []string) { levels := slices.Clone(values) slices.Sort(levels) levels = slices.Compact(levels) for _, lv := range levels[1:] { col := make([]float64, len(values)) for i, v := range values { if v == lv { col[i] = 1 } } cols = append(cols, col) names = append(names, lv) } return cols, names }
Four questions. Answer each one in your head — or out loud — before you open the answer.
In ŷ = 20 + 13·size + 11·beds − 0.8·age, what does −0.8 mean?
B — A multiple-regression coefficient is a comparison holding the other predictors fixed. It's an association, not proof that ageing a house lowers its price.
You add a column of random numbers to a regression. What happens to R² and adjusted R²?
B — Any extra column can only reduce SSE, so R² can't fall. Adjusted R² charges for the extra parameter, and noise rarely earns it back.
Size and bedroom count are strongly correlated. What does that usually do to their coefficients?
B — The data can't tell which of two predictors that move together deserves the credit. Module 9 measures this with the variance inflation factor.
A "region" variable has 4 categories. How many dummy columns go into a model with an intercept?
B — One per category except the baseline. With all four, the dummies add up to the intercept column and XᵀX can't be inverted (the dummy-variable trap).
Commit your work before you move on to module 9, so git log shows your own progress through the course.
Fit the best straight line through a cloud of points, measure how good it is, and put honest error bars on its slope and its predictions.
9A model always produces numbers. Leverage, Cook's distance, variance inflation, and cross-validation tell you whether to believe them.
⌂All ten modules, the course download, and how to track your progress.